Files
katrain-qt/katrain/core/tsumego_frame.py
2ffb79afa2 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>
2022-03-24 11:41:20 +01:00

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