initial move to github
This commit is contained in:
1 parent
336a8fb41b
commit
acd5923ab5
14 files changed
+1582
No files matched your search
@@ -1,3 +1,11 @@
|
||||
# Mine
|
||||
KataGoData
|
||||
old
|
||||
models
|
||||
.idea
|
||||
gtp.log
|
||||
*.zip
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
@@ -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
|
||||
* ....
|
||||
Whitespace-only changes.
+46
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
@@ -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))
|
||||
Binary file not shown.
@@ -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)}]"
|
||||
Reference in new issue
Block a user