1.11 (#492)
* Shade estimated territory (#488) * prune branch, icons * Tsumego frame (#506) * straighten quotes * Bump pillow from 9.0.0 to 9.0.1 (#511) * Bump pillow from 8.3.2 to 9.0.0 (#500) * Add option to display higher precision point loss values (#514) * fix mac build (#516) * drop 3.6 support * new katago Co-authored-by: Miles Crawford <miles@milescrawford.com> Co-authored-by: kaorahi <38910552+kaorahi@users.noreply.github.com> Co-authored-by: Hiraoka <khi@users.osdn.me> Co-authored-by: Zironic <Tony-Liou@users.noreply.github.com> Co-authored-by: pdeblanc <peter@spaceandgames.com> Co-authored-by: Dontbtme <66470440+Dontbtme@users.noreply.github.com>
This commit is contained in:
51 files changed
+2016
-271
No files matched your search
@@ -1,7 +1,7 @@
|
||||
PROGRAM_NAME = "KaTrain"
|
||||
VERSION = "1.10.1"
|
||||
VERSION = "1.11.0"
|
||||
HOMEPAGE = "https://github.com/sanderland/katrain"
|
||||
CONFIG_MIN_VERSION = "1.10.0" # keep config files from this version
|
||||
CONFIG_MIN_VERSION = "1.11.0" # keep config files from this version
|
||||
ANALYSIS_FORMAT_VERSION = "1.0"
|
||||
DATA_FOLDER = "~/.katrain"
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import random
|
||||
import shlex
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
@@ -43,7 +44,7 @@ class KataGoContributeEngine(BaseEngine):
|
||||
self.visits_count = 0
|
||||
self.start_time = 0
|
||||
self.server_error = None
|
||||
|
||||
self.paused = False
|
||||
self.save_sgf = self.config.get("savesgf", False)
|
||||
self.save_path = self.config.get("savepath", "./dist_sgf/")
|
||||
self.move_speed = self.config.get("movespeed", 2.0)
|
||||
@@ -136,6 +137,7 @@ class KataGoContributeEngine(BaseEngine):
|
||||
def queries_remaining(self):
|
||||
return 1
|
||||
|
||||
|
||||
def start(self):
|
||||
try:
|
||||
self.katrain.log(f"Starting Distributed KataGo with {self.command}", OUTPUT_INFO)
|
||||
@@ -157,6 +159,7 @@ class KataGoContributeEngine(BaseEngine):
|
||||
OUTPUT_ERROR,
|
||||
)
|
||||
return # don't start
|
||||
self.paused = False
|
||||
self.stdout_thread = threading.Thread(target=self._read_stdout_thread, daemon=True)
|
||||
self.stderr_thread = threading.Thread(target=self._read_stderr_thread, daemon=True)
|
||||
self.stdout_thread.start()
|
||||
@@ -180,6 +183,8 @@ class KataGoContributeEngine(BaseEngine):
|
||||
def shutdown(self, finish=False):
|
||||
process = self.katago_process
|
||||
if process:
|
||||
self.katago_process.stdin.write(b"forcequit\n")
|
||||
self.katago_process.stdin.flush()
|
||||
self.katago_process = None
|
||||
process.terminate()
|
||||
if finish is not None:
|
||||
@@ -187,6 +192,26 @@ class KataGoContributeEngine(BaseEngine):
|
||||
if t:
|
||||
t.join()
|
||||
|
||||
def graceful_shutdown(self):
|
||||
"""respond to esc"""
|
||||
if self.katago_process:
|
||||
self.katago_process.stdin.write(b"quit\n")
|
||||
self.katago_process.stdin.flush()
|
||||
self.katrain.log("Finishing games in progress and stopping contribution", OUTPUT_KATAGO_STDERR)
|
||||
|
||||
def pause(self):
|
||||
"""respond to pause"""
|
||||
if self.katago_process:
|
||||
if not self.paused:
|
||||
self.katago_process.stdin.write(b"pause\n")
|
||||
self.katago_process.stdin.flush()
|
||||
self.katrain.log("Pausing contribution", OUTPUT_KATAGO_STDERR)
|
||||
else:
|
||||
self.katago_process.stdin.write(b"resume\n")
|
||||
self.katago_process.stdin.flush()
|
||||
self.katrain.log("Resuming contribution", OUTPUT_KATAGO_STDERR)
|
||||
self.paused = not self.paused
|
||||
|
||||
def _read_stderr_thread(self):
|
||||
while self.katago_process is not None:
|
||||
try:
|
||||
|
||||
@@ -65,7 +65,6 @@ class BaseGame:
|
||||
if move_tree:
|
||||
self.root = move_tree
|
||||
self.external_game = PROGRAM_NAME not in self.root.get_property("AP", "")
|
||||
self.komi = self.root.komi
|
||||
handicap = int(self.root.handicap)
|
||||
num_starting_moves_black = 0
|
||||
node = self.root
|
||||
@@ -99,7 +98,6 @@ class BaseGame:
|
||||
**(game_properties or {}),
|
||||
}
|
||||
)
|
||||
self.komi = self.root.komi
|
||||
handicap = katrain.config("game/handicap")
|
||||
if not bypass_config and handicap:
|
||||
self.root.place_handicap_stones(handicap)
|
||||
@@ -291,6 +289,10 @@ class BaseGame:
|
||||
if stop_on_mistake is None:
|
||||
self.set_current_node(cn)
|
||||
|
||||
@property
|
||||
def komi(self):
|
||||
return self.root.komi
|
||||
|
||||
@property
|
||||
def board_size(self):
|
||||
return self.root.board_size
|
||||
|
||||
@@ -11,7 +11,7 @@ from katrain.gui.theme import Theme
|
||||
class Lang(Observable):
|
||||
observers = []
|
||||
callbacks = []
|
||||
FONTS = {"jp": "NotoSansJP-Regular.otf"}
|
||||
FONTS = {"jp": "NotoSansJP-Regular.otf", "tr": "NotoSans-Regular.ttf"}
|
||||
|
||||
def __init__(self, lang):
|
||||
super(Lang, self).__init__()
|
||||
|
||||
@@ -14,7 +14,7 @@ class ParseError(Exception):
|
||||
|
||||
class Move:
|
||||
GTP_COORD = list("ABCDEFGHJKLMNOPQRSTUVWXYZ") + [
|
||||
xa + c for xa in "AB" for c in "ABCDEFGHJKLMNOPQRSTUVWXYZ"
|
||||
xa + c for xa in "ABCDEFGH" for c in "ABCDEFGHJKLMNOPQRSTUVWXYZ"
|
||||
] # board size 52+ support
|
||||
PLAYERS = "BW"
|
||||
SGF_COORD = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ".lower()) + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ") # sgf goes to 52
|
||||
|
||||
Executable
+289
@@ -0,0 +1,289 @@
|
||||
from katrain.core.game_node import GameNode
|
||||
from katrain.core.sgf_parser import Move
|
||||
|
||||
# tsumego frame ported from lizgoban by kaorahi
|
||||
# note: coords = (j, i) in katrain
|
||||
|
||||
near_to_edge = 2
|
||||
offence_to_win = 5
|
||||
|
||||
BLACK = "B"
|
||||
WHITE = "W"
|
||||
|
||||
|
||||
def tsumego_frame_from_katrain_game(game, komi, black_to_play_p, ko_p, margin):
|
||||
current_node = game.current_node
|
||||
bw_board = [[game.chains[c][0].player if c >= 0 else "-" for c in line] for line in game.board]
|
||||
isize, jsize = ij_sizes(bw_board)
|
||||
blacks, whites, analysis_region = tsumego_frame(bw_board, komi, black_to_play_p, ko_p, margin)
|
||||
sgf_blacks = katrain_sgf_from_ijs(blacks, isize, jsize, "B")
|
||||
sgf_whites = katrain_sgf_from_ijs(whites, isize, jsize, "W")
|
||||
|
||||
played_node = GameNode(parent=current_node, properties={"AB": sgf_blacks, "AW": sgf_whites}) # this inserts
|
||||
|
||||
katrain_region = analysis_region and (analysis_region[1], analysis_region[0])
|
||||
return (played_node, katrain_region)
|
||||
|
||||
|
||||
def katrain_sgf_from_ijs(ijs, isize, jsize, player):
|
||||
return [Move((j, i)).sgf((jsize, isize)) for i, j in ijs]
|
||||
|
||||
|
||||
def tsumego_frame(bw_board, komi, black_to_play_p, ko_p, margin):
|
||||
stones = stones_from_bw_board(bw_board)
|
||||
filled_stones = tsumego_frame_stones(stones, komi, black_to_play_p, ko_p, margin)
|
||||
region_pos = pick_all(filled_stones, "tsumego_frame_region_mark")
|
||||
bw = pick_all(filled_stones, "tsumego_frame")
|
||||
blacks = [(i, j) for i, j, black in bw if black]
|
||||
whites = [(i, j) for i, j, black in bw if not black]
|
||||
return (blacks, whites, get_analysis_region(region_pos))
|
||||
|
||||
|
||||
def pick_all(stones, key):
|
||||
return [[i, j, s.get("black")] for i, row in enumerate(stones) for j, s in enumerate(row) if s.get(key)]
|
||||
|
||||
|
||||
def get_analysis_region(region_pos):
|
||||
if len(region_pos) == 0:
|
||||
return None
|
||||
ai, aj, dummy = tuple(zip(*region_pos))
|
||||
ri = (min(ai), max(ai))
|
||||
rj = (min(aj), max(aj))
|
||||
return ri[0] < ri[1] and rj[0] < rj[1] and (ri, rj)
|
||||
|
||||
|
||||
def tsumego_frame_stones(stones, komi, black_to_play_p, ko_p, margin):
|
||||
sizes = ij_sizes(stones)
|
||||
isize, jsize = sizes
|
||||
ijs = [
|
||||
{"i": i, "j": j, "black": h.get("black")}
|
||||
for i, row in enumerate(stones)
|
||||
for j, h in enumerate(row)
|
||||
if h.get("stone")
|
||||
]
|
||||
|
||||
if len(ijs) == 0:
|
||||
return []
|
||||
# find range of problem
|
||||
top = min_by(ijs, "i", +1)
|
||||
left = min_by(ijs, "j", +1)
|
||||
bottom = min_by(ijs, "i", -1)
|
||||
right = min_by(ijs, "j", -1)
|
||||
imin = snap0(top["i"])
|
||||
jmin = snap0(left["j"])
|
||||
imax = snapS(bottom["i"], isize)
|
||||
jmax = snapS(right["j"], jsize)
|
||||
# flip/rotate for standard position
|
||||
# don't mix flip and swap (FF = SS = identity, but SFSF != identity)
|
||||
flip_spec = (
|
||||
[False, False, True] if imin < jmin else [need_flip_p(imin, imax, isize), need_flip_p(jmin, jmax, jsize), False]
|
||||
)
|
||||
if True in flip_spec:
|
||||
flipped = flip_stones(stones, flip_spec)
|
||||
filled = tsumego_frame_stones(flipped, komi, black_to_play_p, ko_p, margin)
|
||||
return flip_stones(filled, flip_spec)
|
||||
# put outside stones
|
||||
i0 = imin - margin
|
||||
i1 = imax + margin
|
||||
j0 = jmin - margin
|
||||
j1 = jmax + margin
|
||||
frame_range = [i0, i1, j0, j1]
|
||||
black_to_attack_p = guess_black_to_attack([top, bottom, left, right], sizes)
|
||||
put_border(stones, sizes, frame_range, black_to_attack_p)
|
||||
put_outside(stones, sizes, frame_range, black_to_attack_p, black_to_play_p, komi)
|
||||
put_ko_threat(stones, sizes, frame_range, black_to_attack_p, black_to_play_p, ko_p)
|
||||
return stones
|
||||
|
||||
|
||||
# detect corner/edge/center problems
|
||||
# (avoid putting border stones on the first lines)
|
||||
def snap(k, to):
|
||||
return to if abs(k - to) <= near_to_edge else k
|
||||
|
||||
|
||||
def snap0(k):
|
||||
return snap(k, 0)
|
||||
|
||||
|
||||
def snapS(k, size):
|
||||
return snap(k, size - 1)
|
||||
|
||||
|
||||
def min_by(ary, key, sign):
|
||||
by = [sign * z[key] for z in ary]
|
||||
return ary[by.index(min(by))]
|
||||
|
||||
|
||||
def need_flip_p(kmin, kmax, size):
|
||||
return kmin < size - kmax - 1
|
||||
|
||||
|
||||
def guess_black_to_attack(extrema, sizes):
|
||||
return sum([sign_of_color(z) * height2(z, sizes) for z in extrema]) > 0
|
||||
|
||||
|
||||
def sign_of_color(z):
|
||||
return 1 if z["black"] else -1
|
||||
|
||||
|
||||
def height2(z, sizes):
|
||||
isize, jsize = sizes
|
||||
return height(z["i"], isize) + height(z["j"], jsize)
|
||||
|
||||
|
||||
def height(k, size):
|
||||
return size - abs(k - (size - 1) / 2)
|
||||
|
||||
|
||||
######################################
|
||||
# sub
|
||||
|
||||
|
||||
def put_border(stones, sizes, frame_range, is_black):
|
||||
i0, i1, j0, j1 = frame_range
|
||||
put_twin(stones, sizes, i0, i1, j0, j1, is_black, False)
|
||||
put_twin(stones, sizes, j0, j1, i0, i1, is_black, True)
|
||||
|
||||
|
||||
def put_twin(stones, sizes, beg, end, at0, at1, is_black, reverse_p):
|
||||
for at in (at0, at1):
|
||||
for k in range(beg, end + 1):
|
||||
i, j = (at, k) if reverse_p else (k, at)
|
||||
put_stone(stones, sizes, i, j, is_black, False, True)
|
||||
|
||||
|
||||
def put_outside(stones, sizes, frame_range, black_to_attack_p, black_to_play_p, komi):
|
||||
isize, jsize = sizes
|
||||
count = 0
|
||||
offense_komi = (+1 if black_to_attack_p else -1) * komi
|
||||
defense_area = (isize * jsize - offense_komi - offence_to_win) / 2
|
||||
for i in range(isize):
|
||||
for j in range(jsize):
|
||||
if inside_p(i, j, frame_range):
|
||||
continue
|
||||
count += 1
|
||||
black_p = xor(black_to_attack_p, (count <= defense_area))
|
||||
empty_p = (i + j) % 2 == 0 and abs(count - defense_area) > isize
|
||||
put_stone(stones, sizes, i, j, black_p, empty_p)
|
||||
|
||||
|
||||
# standard position:
|
||||
# ? = problem, X = offense, O = defense
|
||||
# OOOOOOOOOOOOO
|
||||
# OOOOOOOOOOOOO
|
||||
# OOOOOOOOOOOOO
|
||||
# XXXXXXXXXXXXX
|
||||
# XXXXXXXXXXXXX
|
||||
# XXXX.........
|
||||
# XXXX.XXXXXXXX
|
||||
# XXXX.X???????
|
||||
# XXXX.X???????
|
||||
|
||||
# (pattern, top_p, left_p)
|
||||
offense_ko_threat = (
|
||||
"""
|
||||
....OOOX.
|
||||
.....XXXX
|
||||
""",
|
||||
True,
|
||||
False,
|
||||
)
|
||||
|
||||
defense_ko_threat = (
|
||||
"""
|
||||
..
|
||||
..
|
||||
X.
|
||||
XO
|
||||
OO
|
||||
.O
|
||||
""",
|
||||
False,
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
def put_ko_threat(stones, sizes, frame_range, black_to_attack_p, black_to_play_p, ko_p):
|
||||
isize, jsize = sizes
|
||||
for_offense_p = xor(ko_p, xor(black_to_attack_p, black_to_play_p))
|
||||
pattern, top_p, left_p = offense_ko_threat if for_offense_p else defense_ko_threat
|
||||
aa = [list(line) for line in pattern.splitlines() if len(line) > 0]
|
||||
height, width = ij_sizes(aa)
|
||||
for i, row in enumerate(aa):
|
||||
for j, ch in enumerate(row):
|
||||
ai = i + (0 if top_p else isize - height)
|
||||
aj = j + (0 if left_p else jsize - width)
|
||||
if inside_p(ai, aj, frame_range):
|
||||
return
|
||||
black = xor(black_to_attack_p, ch == "O")
|
||||
empty = ch == "."
|
||||
put_stone(stones, sizes, ai, aj, black, empty)
|
||||
|
||||
|
||||
def xor(a, b):
|
||||
return bool(a) != bool(b)
|
||||
|
||||
|
||||
######################################
|
||||
# util
|
||||
|
||||
|
||||
def flip_stones(stones, flip_spec):
|
||||
swap_p = flip_spec[2]
|
||||
sizes = ij_sizes(stones)
|
||||
isize, jsize = sizes
|
||||
new_isize, new_jsize = [jsize, isize] if swap_p else [isize, jsize]
|
||||
new_stones = [[None for z in range(new_jsize)] for row in range(new_isize)]
|
||||
for i, row in enumerate(stones):
|
||||
for j, z in enumerate(row):
|
||||
new_i, new_j = flip_ij((i, j), sizes, flip_spec)
|
||||
new_stones[new_i][new_j] = z
|
||||
return new_stones
|
||||
|
||||
|
||||
def put_stone(stones, sizes, i, j, black, empty, tsumego_frame_region_mark=False):
|
||||
isize, jsize = sizes
|
||||
if i < 0 or isize <= i or j < 0 or jsize <= j:
|
||||
return
|
||||
stones[i][j] = (
|
||||
{}
|
||||
if empty
|
||||
else {
|
||||
"stone": True,
|
||||
"tsumego_frame": True,
|
||||
"black": black,
|
||||
"tsumego_frame_region_mark": tsumego_frame_region_mark,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def inside_p(i, j, region):
|
||||
i0, i1, j0, j1 = region
|
||||
return i0 <= i and i <= i1 and j0 <= j and j <= j1
|
||||
|
||||
|
||||
def stones_from_bw_board(bw_board):
|
||||
return [[stone_from_str(s) for s in row] for row in bw_board]
|
||||
|
||||
|
||||
def stone_from_str(s):
|
||||
black = s == BLACK
|
||||
white = s == WHITE
|
||||
return {"stone": True, "black": black} if (black or white) else {}
|
||||
|
||||
|
||||
def ij_sizes(stones):
|
||||
return (len(stones), len(stones[0]))
|
||||
|
||||
|
||||
def flip_ij(ij, sizes, flip_spec):
|
||||
i, j = ij
|
||||
isize, jsize = sizes
|
||||
flip_i, flip_j, swap_ij = flip_spec
|
||||
fi = flip1(i, isize, flip_i)
|
||||
fj = flip1(j, jsize, flip_j)
|
||||
return (fj, fi) if swap_ij else (fi, fj)
|
||||
|
||||
|
||||
def flip1(k, size, flag):
|
||||
return size - 1 - k if flag else k
|
||||
Reference in new issue
Block a user