feat: add cryptography fundamentals lab scripts

This commit is contained in:
Matias Tillerias
2026-09-28 19:56:21 -03:00
commit dbc5d68bc9
6 changed files with 1913 additions and 0 deletions

599
Lab0_cifrados_clasicos.py Normal file
View File

@@ -0,0 +1,599 @@
#!/usr/bin/env python3
"""
cifrados_clasicos.py — Tres Cifrados Clásicos en Tabs
Herramienta pedagógica · Fundamentos de Criptografía
Requiere Python 3.x (tkinter incluido en stdlib)
Ejecutar: python cifrados_clasicos.py
"""
import tkinter as tk
from tkinter import ttk
import string, math, random
ALPHA = string.ascii_uppercase
N = 26
# Frecuencias del español (%)
FREQ_ES = {
'A':12.53,'B':1.42,'C':4.68,'D':5.86,'E':13.68,'F':0.69,'G':1.01,
'H':0.70,'I':6.25,'J':0.44,'K':0.02,'L':4.97,'M':3.15,'N':6.71,
'O':8.68,'P':2.51,'Q':0.88,'R':6.87,'S':7.98,'T':4.63,'U':3.93,
'V':0.90,'W':0.01,'X':0.22,'Y':0.90,'Z':0.52,
}
P = dict(
bg='#0d0d1a', panel='#13132b', panel2='#1a1a35',
border='#2a2a50', text='#dde1f0', muted='#555580',
accent='#818cf8', ac='#38bdf8', bc='#fb923c',
rc='#4ade80', btn='#4f46e5', hl='#fde68a',
)
def mk_btn(parent, label, color, cmd, **kw):
return tk.Button(parent, text=label, font=('Consolas', 9, 'bold'),
bg=color, fg='white', relief=tk.FLAT,
padx=10, pady=5, cursor='hand2', command=cmd, **kw)
class CifradosClasicos:
def __init__(self, root):
self.root = root
root.title('Cifrados Clásicos — Fundamentos de Criptografía')
root.configure(bg=P['bg'])
root.geometry('1080x760')
root.minsize(860, 620)
self.shift_var = tk.IntVar(value=3)
self.cmode_var = tk.StringVar(value='E')
self.vig_plain = tk.StringVar()
self.vig_key = tk.StringVar(value='CLAVE')
self.vig_out = tk.StringVar()
self.sub_mode = tk.StringVar(value='E')
self.sub_key = list(ALPHA)
random.shuffle(self.sub_key)
# caches
self._vig_p = self._vig_k = self._vig_c = []
self._sub_result = ''
self._sub_mapping = {}
self._build()
# ── Scaffold ──────────────────────────────────────────────────────────────
def _build(self):
bar = tk.Frame(self.root, bg=P['panel'], pady=10)
bar.pack(fill=tk.X)
tk.Label(bar, text='🔐 Cifrados Clásicos',
font=('Consolas', 15, 'bold'),
bg=P['panel'], fg=P['accent']).pack(side=tk.LEFT, padx=18)
tk.Label(bar,
text='César · Vigenère · Sustitución + Análisis de Frecuencia',
font=('Consolas', 9), bg=P['panel'], fg=P['muted']
).pack(side=tk.RIGHT, padx=18)
st = ttk.Style()
st.theme_use('default')
st.configure('C.TNotebook', background=P['bg'], borderwidth=0)
st.configure('C.TNotebook.Tab',
background=P['panel2'], foreground=P['muted'],
padding=[22, 8], font=('Consolas', 10, 'bold'))
st.map('C.TNotebook.Tab',
background=[('selected', P['btn'])],
foreground=[('selected', 'white')])
nb = ttk.Notebook(self.root, style='C.TNotebook')
nb.pack(fill=tk.BOTH, expand=True, padx=12, pady=10)
for tab_text, fn in [
(' César / ROT-N ', self._build_caesar),
(' Vigenère ', self._build_vigenere),
(' Sustitución + Frecuencias ', self._build_substitution),
]:
f = tk.Frame(nb, bg=P['bg'])
nb.add(f, text=tab_text)
fn(f)
# ══════════════════════════════════════════════════════════════════════════
# TAB 1 · CÉSAR / ROT-N
# ══════════════════════════════════════════════════════════════════════════
def _build_caesar(self, parent):
left = tk.Frame(parent, bg=P['bg'], padx=16, pady=12)
left.pack(side=tk.LEFT, fill=tk.Y)
right = tk.Frame(parent, bg=P['bg'], pady=12)
right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Slider desplazamiento
tk.Label(left, text='Desplazamiento N', font=('Consolas', 10, 'bold'),
bg=P['bg'], fg=P['accent']).pack(anchor=tk.W)
srow = tk.Frame(left, bg=P['bg'])
srow.pack(fill=tk.X, pady=(4, 12))
tk.Scale(srow, from_=0, to=25, orient=tk.HORIZONTAL,
variable=self.shift_var, command=self._on_shift,
bg=P['panel2'], fg=P['text'], highlightthickness=0,
troughcolor=P['border'], length=230,
font=('Consolas', 9)).pack(side=tk.LEFT)
self.c_shift_lbl = tk.Label(srow, text='N = 3',
font=('Consolas', 13, 'bold'),
bg=P['bg'], fg=P['hl'], padx=8)
self.c_shift_lbl.pack(side=tk.LEFT)
# Modo
mrow = tk.Frame(left, bg=P['bg'])
mrow.pack(fill=tk.X, pady=(0, 12))
for txt, val, col in [('Cifrar', 'E', P['ac']), ('Descifrar', 'D', P['bc'])]:
tk.Radiobutton(mrow, text=txt, variable=self.cmode_var, value=val,
command=self._update_caesar,
bg=P['bg'], fg=col, selectcolor=P['panel2'],
activebackground=P['bg'],
font=('Consolas', 10, 'bold')
).pack(side=tk.LEFT, padx=(0, 18))
# Input / output
for attr, lbl_txt, fg, editable in [
('c_in', 'Texto de entrada', P['ac'], True),
('c_out', 'Resultado', P['rc'], False),
]:
tk.Label(left, text=lbl_txt, font=('Consolas', 9),
bg=P['bg'], fg=P['muted']).pack(anchor=tk.W)
w = tk.Text(left, height=4, width=33,
font=('Consolas', 11), wrap=tk.WORD,
bg=P['panel2'] if editable else P['panel'],
fg=fg, relief=tk.FLAT, insertbackground=fg,
state=tk.NORMAL if editable else tk.DISABLED)
w.pack(pady=(2, 10))
setattr(self, attr, w)
self.c_in.bind('<KeyRelease>', lambda e: self._update_caesar())
self.c_in.insert('1.0', 'Hola Mundo')
self.c_formula = tk.Label(left, text='', font=('Consolas', 9),
bg=P['bg'], fg=P['muted'], justify=tk.LEFT)
self.c_formula.pack(anchor=tk.W)
# Rueda
tk.Label(right, text='Rueda de César',
font=('Consolas', 10, 'bold'),
bg=P['bg'], fg=P['accent']).pack()
self.c_cv = tk.Canvas(right, width=430, height=430,
bg=P['bg'], highlightthickness=0)
self.c_cv.pack(pady=4)
self._update_caesar()
def _on_shift(self, val=None):
self.c_shift_lbl.config(text=f'N = {self.shift_var.get()}')
self._update_caesar()
def _draw_wheel(self, hl_plain=None, hl_cipher=None):
cv = self.c_cv
cv.delete('all')
shift = self.shift_var.get()
cx = cy = 215
R1, R2 = 190, 145 # outer / inner ring radius
T1, T2 = 172, 126 # text radii
# Rings
cv.create_oval(cx-R1, cy-R1, cx+R1, cy+R1,
fill=P['panel2'], outline=P['border'], width=2)
cv.create_oval(cx-R2, cy-R2, cx+R2, cy+R2,
fill=P['panel'], outline=P['accent'], width=2)
cv.create_oval(cx-105, cy-105, cx+105, cy+105,
fill=P['panel2'], outline=P['border'], width=1)
# Segment dividers
for i in range(N):
a = math.radians(-90 + i * 360/N)
xa = cx + R2 * math.cos(a); ya = cy + R2 * math.sin(a)
xb = cx + R1 * math.cos(a); yb = cy + R1 * math.sin(a)
cv.create_line(xa, ya, xb, yb, fill=P['border'], width=1)
for i in range(N):
angle = math.radians(-90 + i * 360/N)
# Outer: texto plano (fijo)
plain_letter = ALPHA[i]
xo = cx + T1 * math.cos(angle)
yo = cy + T1 * math.sin(angle)
is_hl = (hl_plain and plain_letter == hl_plain)
if is_hl:
cv.create_oval(xo-14, yo-14, xo+14, yo+14,
fill='#2d2200', outline=P['hl'], width=2)
cv.create_text(xo, yo, text=plain_letter,
font=('Consolas', 11, 'bold'),
fill=P['hl'] if is_hl else P['text'])
# Inner: cifrado (girado por shift)
cipher_letter = ALPHA[(i + shift) % N]
xi = cx + T2 * math.cos(angle)
yi = cy + T2 * math.sin(angle)
is_hl2 = (hl_cipher and cipher_letter == hl_cipher)
if is_hl2:
cv.create_oval(xi-14, yi-14, xi+14, yi+14,
fill='#0a2510', outline=P['rc'], width=2)
cv.create_text(xi, yi, text=cipher_letter,
font=('Consolas', 11, 'bold'),
fill=P['rc'] if is_hl2 else P['bc'])
# Centro
cv.create_text(cx, cy - 10, text=f'N = {shift}',
font=('Consolas', 14, 'bold'), fill=P['accent'])
cv.create_text(cx, cy + 12, text='C = (P+N) mod 26',
font=('Consolas', 8), fill=P['muted'])
# Flecha indicadora en la cima
tip_x = cx + (R1 + 8) * math.cos(math.radians(-90))
tip_y = cy + (R1 + 8) * math.sin(math.radians(-90))
cv.create_polygon(tip_x - 7, tip_y, tip_x + 7, tip_y, tip_x, tip_y - 18,
fill=P['hl'])
# Etiquetas de anillos
cv.create_text(cx, cy - T1 - 22, text='Texto plano (fijo)',
font=('Consolas', 8), fill=P['ac'])
cv.create_text(cx, cy - T2 + 26, text='Cifrado (gira)',
font=('Consolas', 8), fill=P['bc'])
def _update_caesar(self, *_):
text = self.c_in.get('1.0', tk.END).strip().upper()
shift = self.shift_var.get()
sgn = 1 if self.cmode_var.get() == 'E' else -1
result = ''
for ch in text:
result += ALPHA[(ALPHA.index(ch) + sgn * shift) % N] if ch in ALPHA else ch
self.c_out.config(state=tk.NORMAL)
self.c_out.delete('1.0', tk.END)
self.c_out.insert('1.0', result)
self.c_out.config(state=tk.DISABLED)
first_p = next((c for c in text if c in ALPHA), None)
first_c = ALPHA[(ALPHA.index(first_p) + sgn * shift) % N] if first_p else None
if self.cmode_var.get() == 'E':
self.c_formula.config(
text=f'Fórmula: C = (P + {shift}) mod 26\n'
f'Ejemplo: A(0) → {ALPHA[shift % N]}({shift})')
else:
self.c_formula.config(
text=f'Fórmula: P = (C − {shift}) mod 26\n'
f'Ejemplo: {ALPHA[shift % N]}({shift}) → A(0)')
self._draw_wheel(hl_plain=first_p, hl_cipher=first_c)
# ══════════════════════════════════════════════════════════════════════════
# TAB 2 · VIGENÈRE
# ══════════════════════════════════════════════════════════════════════════
def _build_vigenere(self, parent):
top = tk.Frame(parent, bg=P['bg'], padx=14, pady=10)
top.pack(fill=tk.X)
# IO
io = tk.Frame(top, bg=P['bg'])
io.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 18))
for attr, lbl_txt, var, fg, editable in [
('v_plain_e', 'Texto plano', self.vig_plain, P['ac'], True),
('v_key_e', 'Clave', self.vig_key, P['bc'], True),
('v_out_e', 'Cifrado', self.vig_out, P['rc'], False),
]:
tk.Label(io, text=lbl_txt, font=('Consolas', 9),
bg=P['bg'], fg=P['muted']).pack(anchor=tk.W)
e = tk.Entry(io, textvariable=var,
font=('Consolas', 12, 'bold'), relief=tk.FLAT,
bg=P['panel2'] if editable else P['panel'],
fg=fg, insertbackground=fg, width=26,
state=tk.NORMAL if editable else tk.DISABLED)
e.pack(pady=(2, 8), fill=tk.X)
setattr(self, attr, e)
self.vig_plain.trace_add('write', self._update_vig)
self.vig_key.trace_add('write', self._update_vig)
self.vig_plain.set('HOLA')
self.v_explain = tk.Label(io, text='', font=('Consolas', 8),
bg=P['bg'], fg=P['muted'],
justify=tk.LEFT, wraplength=340)
self.v_explain.pack(anchor=tk.W, pady=(6, 0))
# Mapa char a char
map_f = tk.Frame(top, bg=P['bg'])
map_f.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
tk.Label(map_f, text='Mapeo carácter a carácter',
font=('Consolas', 9, 'bold'),
bg=P['bg'], fg=P['accent']).pack(anchor=tk.W)
self.v_map_cv = tk.Canvas(map_f, height=100, bg=P['panel2'],
highlightthickness=0)
self.v_map_cv.pack(fill=tk.X, pady=(4, 0))
self.v_map_cv.bind('<Configure>', lambda e: self._draw_vig_map())
# Tabla Vigenère (scrollable)
tk.Label(parent,
text='Tabla de Vigenère (fila=clave, columna=plano, celda=cifrado)',
font=('Consolas', 8, 'bold'), bg=P['bg'], fg=P['muted']
).pack(padx=14, anchor=tk.W)
tf = tk.Frame(parent, bg=P['bg'])
tf.pack(fill=tk.BOTH, expand=True, padx=14, pady=(2, 10))
self.v_tbl = tk.Canvas(tf, bg=P['panel2'], highlightthickness=0)
vb = ttk.Scrollbar(tf, orient=tk.VERTICAL, command=self.v_tbl.yview)
hb = ttk.Scrollbar(tf, orient=tk.HORIZONTAL, command=self.v_tbl.xview)
self.v_tbl.configure(yscrollcommand=vb.set, xscrollcommand=hb.set)
hb.pack(side=tk.BOTTOM, fill=tk.X)
vb.pack(side=tk.RIGHT, fill=tk.Y)
self.v_tbl.pack(fill=tk.BOTH, expand=True)
self._draw_vig_table()
def _draw_vig_table(self, hr=-1, hc=-1):
cv = self.v_tbl
cv.delete('all')
CS = 20
for r in range(27):
for c in range(27):
x, y = c * CS, r * CS
if r == 0 and c == 0:
txt, bg, fg = '', P['panel'], P['muted']
elif r == 0:
li = c - 1; hl = (li == hc)
txt = ALPHA[li]
bg = P['ac'] if hl else P['panel']
fg = P['bg'] if hl else P['ac']
elif c == 0:
li = r - 1; hl = (li == hr)
txt = ALPHA[li]
bg = P['bc'] if hl else P['panel']
fg = P['bg'] if hl else P['bc']
else:
letter = ALPHA[(r - 1 + c - 1) % N]
active = (r-1 == hr and c-1 == hc)
in_r = (r-1 == hr)
in_c = (c-1 == hc)
txt = letter
if active:
bg, fg = P['rc'], P['bg']
elif in_r:
bg, fg = '#162210', P['rc']
elif in_c:
bg, fg = '#101630', P['ac']
else:
bg, fg = P['panel2'], P['muted']
cv.create_rectangle(x, y, x+CS, y+CS,
fill=bg, outline=P['bg'], width=1)
cv.create_text(x+CS//2, y+CS//2, text=txt,
font=('Consolas', 8, 'bold'
if r == 0 or c == 0 or (r-1==hr and c-1==hc)
else 'normal'),
fill=fg)
total = 27 * CS
cv.configure(scrollregion=(0, 0, total, total))
def _update_vig(self, *_):
plain = ''.join(c for c in self.vig_plain.get().upper() if c in ALPHA)
key = ''.join(c for c in self.vig_key.get().upper() if c in ALPHA)
self.v_out_e.config(state=tk.NORMAL)
if not plain or not key:
self.vig_out.set('')
self.v_out_e.config(state=tk.DISABLED)
self._draw_vig_table()
return
cipher = [ALPHA[(ALPHA.index(p) + ALPHA.index(key[i % len(key)])) % N]
for i, p in enumerate(plain)]
self.vig_out.set(''.join(cipher))
self.v_out_e.config(state=tk.DISABLED)
p0 = ALPHA.index(plain[0]); k0 = ALPHA.index(key[0]); c0 = (p0+k0) % N
self._draw_vig_table(hr=k0, hc=p0)
self.v_explain.config(
text=(f"'{plain[0]}' (col {p0}) + clave '{key[0]}' (fila {k0})\n"
f" → ({p0}+{k0}) mod 26 = {c0} → '{ALPHA[c0]}'"))
self._vig_p = list(plain)
self._vig_k = list(key)
self._vig_c = cipher
self._draw_vig_map()
def _draw_vig_map(self):
cv = self.v_map_cv
cv.delete('all')
pl = self._vig_p; ky = self._vig_k; ci = self._vig_c
if not pl: return
W = cv.winfo_width() or 500
n = min(len(pl), max(1, (W - 72) // 30))
cell = max(26, (W - 72) // max(n, 1))
for ri, (letters, color, y, label) in enumerate([
(pl[:n], P['ac'], 18, 'Plano '),
(ky[:n], P['bc'], 52, 'Clave '),
(ci[:n], P['rc'], 86, 'Cifrado'),
]):
cv.create_text(6, y, text=label, anchor=tk.W,
font=('Consolas', 8), fill=P['muted'])
for j, letter in enumerate(letters):
x = 66 + j * cell + cell // 2
if j == 0:
cv.create_rectangle(x-14, y-12, x+14, y+12,
fill=P['panel'], outline=color, width=2)
cv.create_text(x, y, text=letter,
font=('Consolas', 11, 'bold'), fill=color)
# Marcadores de ciclo de clave
if label == 'Clave ':
kl = len(ky)
for j in range(1, n):
if j % kl == 0:
xm = 66 + j * cell
cv.create_line(xm, 34, xm, 70, fill=P['border'], dash=(3, 3))
cv.create_text(xm, 32, text='↺', font=('Consolas', 7),
fill=P['bc'])
# ══════════════════════════════════════════════════════════════════════════
# TAB 3 · SUSTITUCIÓN + FRECUENCIAS
# ══════════════════════════════════════════════════════════════════════════
def _build_substitution(self, parent):
top = tk.Frame(parent, bg=P['bg'], padx=14, pady=10)
top.pack(fill=tk.X)
left = tk.Frame(top, bg=P['bg'])
left.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 18))
# Modo + shuffle
mrow = tk.Frame(left, bg=P['bg'])
mrow.pack(fill=tk.X, pady=(0, 8))
for txt, val, col in [('Cifrar', 'E', P['ac']), ('Descifrar', 'D', P['bc'])]:
tk.Radiobutton(mrow, text=txt, variable=self.sub_mode, value=val,
command=self._update_sub,
bg=P['bg'], fg=col, selectcolor=P['panel2'],
activebackground=P['bg'],
font=('Consolas', 10, 'bold')
).pack(side=tk.LEFT, padx=(0, 18))
mk_btn(left, '🔀 Nueva clave aleatoria', P['btn'],
self._shuffle_sub).pack(fill=tk.X, pady=(0, 10))
for attr, lbl_txt, fg, editable in [
('s_in', 'Texto de entrada', P['ac'], True),
('s_out', 'Resultado', P['rc'], False),
]:
tk.Label(left, text=lbl_txt, font=('Consolas', 9),
bg=P['bg'], fg=P['muted']).pack(anchor=tk.W)
w = tk.Text(left, height=4, width=36,
font=('Consolas', 11), wrap=tk.WORD,
bg=P['panel2'] if editable else P['panel'],
fg=fg, relief=tk.FLAT, insertbackground=fg,
state=tk.NORMAL if editable else tk.DISABLED)
w.pack(pady=(2, 8))
setattr(self, attr, w)
self.s_in.bind('<KeyRelease>', lambda e: self._update_sub())
self.s_in.insert('1.0', 'ESTAMOS APRENDIENDO CRIPTOGRAFIA')
# Tabla de clave
right = tk.Frame(top, bg=P['bg'])
right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
tk.Label(right, text='Tabla de sustitución (A→? para cada letra)',
font=('Consolas', 9, 'bold'),
bg=P['bg'], fg=P['accent']).pack(anchor=tk.W)
self.s_key_cv = tk.Canvas(right, height=72, bg=P['panel2'],
highlightthickness=0)
self.s_key_cv.pack(fill=tk.X, pady=(4, 0))
self.s_key_cv.bind('<Configure>', lambda e: self._draw_sub_key())
# Histograma de frecuencias
tk.Label(parent,
text='Análisis de Frecuencia (texto cifrado vs. español estándar)',
font=('Consolas', 9, 'bold'), bg=P['bg'], fg=P['muted']
).pack(padx=14, anchor=tk.W, pady=(6, 0))
self.s_freq_cv = tk.Canvas(parent, bg=P['panel2'], highlightthickness=0)
self.s_freq_cv.pack(fill=tk.BOTH, expand=True, padx=14, pady=(4, 10))
self.s_freq_cv.bind('<Configure>', lambda e: self._draw_freq())
self.root.after(120, self._update_sub)
def _shuffle_sub(self):
random.shuffle(self.sub_key)
self._update_sub()
def _update_sub(self, *_):
text = self.s_in.get('1.0', tk.END).strip().upper()
mode = self.sub_mode.get()
fwd = {ALPHA[i]: self.sub_key[i] for i in range(N)}
rev = {v: k for k, v in fwd.items()}
mapping = fwd if mode == 'E' else rev
result = ''.join(mapping.get(ch, ch) if ch in ALPHA else ch for ch in text)
self.s_out.config(state=tk.NORMAL)
self.s_out.delete('1.0', tk.END)
self.s_out.insert('1.0', result)
self.s_out.config(state=tk.DISABLED)
self._sub_result = result
self._sub_mapping = mapping
self._draw_sub_key()
self._draw_freq()
def _draw_sub_key(self):
cv = self.s_key_cv
cv.delete('all')
mapping = self._sub_mapping
if not mapping: return
W = cv.winfo_width() or 700
cw = max(20, W // N)
for i, letter in enumerate(ALPHA):
x = i * cw + cw // 2
mapped = mapping.get(letter, letter)
cv.create_text(x, 14, text=letter, font=('Consolas', 9), fill=P['ac'])
cv.create_text(x, 36, text='↓', font=('Consolas', 8), fill=P['muted'])
cv.create_text(x, 58, text=mapped, font=('Consolas', 9, 'bold'),
fill=P['rc'] if mapped != letter else P['muted'])
def _draw_freq(self):
cv = self.s_freq_cv
cv.delete('all')
W = cv.winfo_width() or 800
H = cv.winfo_height() or 200
if H < 60: return
letters = [c for c in self._sub_result if c in ALPHA]
total = max(len(letters), 1)
ct_freq = {l: letters.count(l) / total * 100 for l in ALPHA}
MAX_F = 16.0
pad_l = 36
pad_b = 28
ch_w = W - pad_l - 10
ch_h = H - pad_b - 20
bot = H - pad_b
top_y = bot - ch_h
gw = ch_w // N
bw = max(3, gw // 2 - 1)
# Eje Y
for pct in [0, 4, 8, 12, 16]:
yp = bot - int(pct / MAX_F * ch_h)
cv.create_line(pad_l - 4, yp, pad_l, yp, fill=P['muted'])
cv.create_text(pad_l - 6, yp, text=f'{pct}%',
anchor=tk.E, font=('Consolas', 7), fill=P['muted'])
# Leyenda
cv.create_rectangle(pad_l, 4, pad_l + 10, 14, fill=P['bc'], outline='')
cv.create_text(pad_l + 14, 9, text='Español estándar',
anchor=tk.W, font=('Consolas', 7), fill=P['muted'])
cv.create_rectangle(pad_l + 130, 4, pad_l + 140, 14, fill=P['rc'], outline='')
cv.create_text(pad_l + 144, 9, text='Texto cifrado',
anchor=tk.W, font=('Consolas', 7), fill=P['muted'])
for i, letter in enumerate(ALPHA):
xb = pad_l + i * gw + 2
# Barra español
es_h = int(min(FREQ_ES[letter], MAX_F) / MAX_F * ch_h)
cv.create_rectangle(xb, bot - es_h, xb + bw, bot,
fill=P['bc'], outline='')
# Barra texto cifrado
ct_h = int(min(ct_freq[letter], MAX_F) / MAX_F * ch_h)
cv.create_rectangle(xb + bw + 1, bot - ct_h,
xb + bw * 2 + 1, bot,
fill=P['rc'], outline='')
cv.create_text(xb + bw, bot + 10, text=letter,
font=('Consolas', 7), fill=P['muted'])
# ── Entry point ───────────────────────────────────────────────────────────────
if __name__ == '__main__':
root = tk.Tk()
app = CifradosClasicos(root)
root.update_idletasks()
w, h = root.winfo_width(), root.winfo_height()
sw, sh = root.winfo_screenwidth(), root.winfo_screenheight()
root.geometry(f'{w}x{h}+{(sw-w)//2}+{(sh-h)//2}')
root.mainloop()