* 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>
290 lines
8.1 KiB
Python
Executable File
290 lines
8.1 KiB
Python
Executable File
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
|