Skip to content

Instantly share code, notes, and snippets.

@felipem775
Last active June 2, 2026 08:37
Show Gist options
  • Select an option

  • Save felipem775/66984e81eea73e1deed6b23e1a57694a to your computer and use it in GitHub Desktop.

Select an option

Save felipem775/66984e81eea73e1deed6b23e1a57694a to your computer and use it in GitHub Desktop.
Convierte un sgf a un PDF de 2 páginas din-a4 con 8 diagramas. Dependencias pip: sgf y matplotlib
import sys
import math
import sgf
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
from matplotlib.gridspec import GridSpec
from matplotlib.backends.backend_pdf import PdfPages
# Requires numpy and sgf packages
def coord_to_go(row, col):
col_letter = chr(ord("a") + col + (1 if col >= 8 else 0))
return f"{col_letter}{row+1}"
def parse_sgf(sgf_content):
collection = sgf.parse(sgf_content)
game = collection[0]
root = game.root
props = root.properties
board_size = int(props.get("SZ", ["19"])[0])
metadata = {
"date": props.get("DT", ["?"])[0],
"black": props.get("PB", ["?"])[0],
"white": props.get("PW", ["?"])[0],
"title": props.get("GN", ["?"])[0],
"result": props.get("RE", [None])[0],
}
moves = []
node = root
while node:
if "B" in node.properties:
color = "b"
coord = node.properties["B"][0]
elif "W" in node.properties:
color = "w"
coord = node.properties["W"][0]
else:
node = node.next
continue
if isinstance(coord, tuple):
row, col = coord
row -= 1
col -= 1
elif isinstance(coord, str) and len(coord) == 2:
col = ord(coord[0]) - ord("a")
row = (board_size - 1) - (ord(coord[1]) - ord("a"))
else:
node = node.next
continue
if 0 <= row < board_size and 0 <= col < board_size:
moves.append((color, (row, col)))
else:
print(f"Advertencia: coordenada {coord} fuera de rango")
node = node.next
return board_size, moves, metadata
def draw_board(ax, board, move_numbers, board_size, start_move, end_move):
ax.clear()
ax.set_aspect("equal")
ax.axis("off")
# Ajuste para coordenadas solo izquierda y arriba
ax.set_xlim(-1.0, board_size - 1 + 0.4)
ax.set_ylim(-0.8, board_size - 1 + 0.6)
# Líneas
for i in range(board_size):
ax.plot([0, board_size - 1], [i, i], "k", lw=1, zorder=1)
ax.plot([i, i], [0, board_size - 1], "k", lw=1, zorder=1)
# Letras arriba
letters = []
for j in range(board_size):
l = chr(ord("a") + j)
if l >= "i":
l = chr(ord(l) + 1)
letters.append(l)
for col in range(board_size):
ax.text(
col,
board_size - 0.6,
letters[col],
ha="center",
va="bottom",
fontsize=6,
family="sans-serif",
zorder=15,
)
# Números izquierda
numbers = [str(i + 1) for i in range(board_size)]
for row in range(board_size):
ax.text(
-0.4,
row,
numbers[row],
ha="right",
va="center",
fontsize=6,
family="sans-serif",
zorder=15,
)
# Hoshi
if board_size == 19:
star_points = [
(3, 3),
(15, 3),
(3, 15),
(15, 15),
(9, 9),
(3, 9),
(15, 9),
(9, 3),
(9, 15),
]
elif board_size == 13:
star_points = [(3, 3), (9, 3), (3, 9), (9, 9), (6, 6)]
elif board_size == 9:
star_points = [(2, 2), (6, 2), (2, 6), (6, 6), (4, 4)]
else:
star_points = []
for x, y in star_points:
ax.plot(x, y, "ko", markersize=4, zorder=1)
# Piedras
for row in range(board_size):
for col in range(board_size):
stone = board[row][col]
if stone is None:
continue
color = "black" if stone == "b" else "white"
circle = Circle(
(col, row),
0.42,
facecolor=color,
edgecolor="black",
linewidth=0.5,
zorder=10,
)
ax.add_patch(circle)
move_num = move_numbers[row][col]
if move_num is not None and start_move <= move_num <= end_move:
text_color = "white" if stone == "b" else "black"
ax.text(
col,
row,
str(move_num),
ha="center",
va="center",
fontsize=6,
weight="normal",
family="sans-serif",
color=text_color,
zorder=11,
)
def render_diagram_moves(
ax,
sm,
em,
moves,
captures_set,
fontsize=7,
items_per_line=7,
result_text=None,
):
"""Renderiza los movimientos de un diagrama debajo de los ejes.
Las jugadas en posiciones ya ocupadas se muestran subrayadas.
Usa fuente monospace para estimar posición de cada token.
Los números de movimiento se muestran en negrita.
Si result_text está presente, se concatena al final de la secuencia.
"""
tokens = []
for j in range(sm - 1, em):
_, (r, c) = moves[j]
move_num = j + 1
coord = coord_to_go(r, c)
tokens.append((move_num, coord, move_num in captures_set))
# Anchura de carácter en fracción de ejes (monospace, layout A4 2 col)
# axes_width ≈ 0.44 * fig_width (2 cols, wspace=0.03, márgenes 0.05–0.95)
# axes_height ≈ 0.80/(2+0.35) * fig_height (2 filas, hspace=0.35, top=0.90, bot=0.10)
ax_w_pt = 0.44 * 8.27 * 72 # ≈ 262 pt
ax_h_pt = 0.80 / 2.35 * 11.69 * 72 # ≈ 286 pt
char_w = fontsize * 0.6 / ax_w_pt # ≈ 0.0137 fracción de eje por carácter
sep_w = 2 * char_w # ", "
underline_dy = (
fontsize * 1.05 / ax_h_pt
) # offset desde top hasta subrayado
y_start = -0.04
line_spacing = 0.055
x_margin = 0.01
for chunk_start in range(0, len(tokens), items_per_line):
chunk = tokens[chunk_start : chunk_start + items_per_line]
line_num = chunk_start // items_per_line
y = y_start - line_num * line_spacing
# Renderizar cada token por separado para aplicar negrita al número
x = x_margin
for tok_idx, (move_num, coord, is_cap) in enumerate(chunk):
if tok_idx > 0:
# Renderizar separador ", "
ax.text(
x,
y,
", ",
ha="left",
va="top",
fontsize=fontsize,
family="monospace",
transform=ax.transAxes,
clip_on=False,
)
x += sep_w
# Renderizar número de movimiento en negrita
num_text = f"{move_num:>3}"
ax.text(
x,
y,
num_text,
ha="left",
va="top",
fontsize=fontsize,
weight="bold",
family="monospace",
transform=ax.transAxes,
clip_on=False,
)
x += len(num_text) * char_w
# Renderizar ":" y coordenadas en normal
coord_text = f":{coord:<3}"
ax.text(
x,
y,
coord_text,
ha="left",
va="top",
fontsize=fontsize,
family="monospace",
transform=ax.transAxes,
clip_on=False,
)
# Dibujar subrayado si es captura
tok_w = (len(num_text) + len(coord_text)) * char_w
if is_cap:
y_ul = y - underline_dy
ax.plot(
[x - len(num_text) * char_w, x + len(coord_text) * char_w],
[y_ul, y_ul],
"k-",
linewidth=0.7,
transform=ax.transAxes,
clip_on=False,
)
x += len(coord_text) * char_w
# Agregar resultado al final si está presente
if result_text:
# Calcular en qué línea continuar
total_tokens = len(tokens)
last_line_tokens = total_tokens % items_per_line
if last_line_tokens == 0:
last_line_tokens = items_per_line
last_line_num = (total_tokens - 1) // items_per_line
y = y_start - last_line_num * line_spacing
# Calcular posición x después del último movimiento
x = x_margin
last_chunk_start = (total_tokens // items_per_line) * items_per_line
for tok_idx in range(last_line_tokens):
if tok_idx > 0:
x += sep_w
x += 3 * char_w # número (3 chars)
x += 4 * char_w # ":" + coordenadas (4 chars)
# Renderizar separador y resultado
ax.text(
x,
y,
" — ",
ha="left",
va="top",
fontsize=fontsize,
family="monospace",
transform=ax.transAxes,
clip_on=False,
)
x += 5 * char_w
ax.text(
x,
y,
result_text,
ha="left",
va="top",
fontsize=fontsize,
weight="bold",
family="monospace",
transform=ax.transAxes,
clip_on=False,
)
def main(sgf_file, pdf_output):
with open(sgf_file, "r", encoding="utf-8") as f:
sgf_content = f.read()
board_size, moves, metadata = parse_sgf(sgf_content)
if not moves:
print("El SGF no contiene movimientos válidos.")
sys.exit(1)
total_moves = len(moves)
# Calcular 8 puntos equidistantes en la partida
indices = [
int(math.floor((i + 1) * (total_moves - 1) / 8.0)) for i in range(7)
]
indices.append(total_moves - 1) # Posición final
if len(set(indices)) < 8:
indices = sorted(set(indices))
while len(indices) < 8:
indices.append(total_moves - 1)
indices = sorted(set(indices))[:8]
# Generar estados para los 8 diagramas
diagram_data = []
for i, idx in enumerate(indices):
board = [[None for _ in range(board_size)] for _ in range(board_size)]
move_numbers = [
[None for _ in range(board_size)] for _ in range(board_size)
]
captures = [] # Lista de capturas en este diagrama
start_move = 1 if i == 0 else indices[i - 1] + 2
for j in range(idx + 1):
color, (row, col) = moves[j]
move_num = j + 1
# Detectar si hay una piedra en esta posición
if board[row][col] is not None and move_num >= start_move:
# Captura detectada
captures.append((move_num, coord_to_go(row, col)))
board[row][col] = color
move_numbers[row][col] = move_num
end_move = idx + 1
diagram_data.append(
(board, move_numbers, start_move, end_move, captures)
)
# Crear PDF con 2 páginas
with PdfPages(pdf_output) as pdf:
# Página 1: primeros 4 diagramas
fig = plt.figure(figsize=(8.27, 11.69))
fig.suptitle(
f"{metadata['date']} {metadata['title']}\n{metadata['black']} (B) vs {metadata['white']} (W)",
fontsize=12,
y=0.96,
)
gs = GridSpec(
1,
1,
top=0.90,
bottom=0.10,
left=0.05,
right=0.95,
)
gs_diag = gs[0].subgridspec(2, 2, hspace=0.35, wspace=0.03)
axes = []
for i in range(2):
for j in range(2):
ax = fig.add_subplot(gs_diag[i, j])
axes.append(ax)
for i, ax in enumerate(axes):
board, mn, sm, em, captures = diagram_data[i]
draw_board(ax, board, mn, board_size, sm, em)
ax.set_title(f"{sm} - {em}", fontsize=10, pad=12)
captures_set = {num for num, _ in captures}
render_diagram_moves(ax, sm, em, moves, captures_set)
pdf.savefig(fig, dpi=300)
plt.close(fig)
# Página 2: últimos 4 diagramas
fig = plt.figure(figsize=(8.27, 11.69))
fig.suptitle(
f"{metadata['date']} {metadata['title']}\n{metadata['black']} (B) vs {metadata['white']} (W)",
fontsize=12,
y=0.96,
)
gs = GridSpec(
1,
1,
top=0.91,
bottom=0.09,
left=0.05,
right=0.95,
)
gs_diag = gs[0].subgridspec(2, 2, hspace=0.25, wspace=0.03)
axes = []
for i in range(2):
for j in range(2):
ax = fig.add_subplot(gs_diag[i, j])
axes.append(ax)
for i, ax in enumerate(axes):
board, mn, sm, em, captures = diagram_data[i + 4]
draw_board(ax, board, mn, board_size, sm, em)
ax.set_title(f"{sm} - {em}", fontsize=10, pad=12)
captures_set = {num for num, _ in captures}
# Pasar resultado solo en el último diagrama
result = (
metadata["result"] if (i == 3 and metadata["result"]) else None
)
render_diagram_moves(
ax, sm, em, moves, captures_set, result_text=result
)
pdf.savefig(fig, dpi=300)
plt.close(fig)
print(f"PDF generado: {pdf_output}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Uso: python sgf_a4_kifu.py archivo.sgf salida.pdf")
sys.exit(1)
main(sys.argv[1], sys.argv[2])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment