Saltar al contenido
Inicio 100 Días de Código en Python — Día 31: Proyecto Final "Flashy", la aplicación de tarjetas de estudio

100 Días de Código en Python — Día 31: Proyecto Final "Flashy", la aplicación de tarjetas de estudio

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 31 (nivel intermedio, proyecto final o capstone).

Este tutorial adapta pedagógicamente el Día 31. Las secciones 📝 Ejercicio original del curso provienen de los archivos del curso. Los retos 🟢🟡🟠🔴, el proyecto extra y el proyecto integrador son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.

Nota sobre las verificaciones: los fragmentos de pandas y los errores se comprobaron con Python 3.14 y pandas 3.0. El proyecto inicial (las carpetas images y data, con french_words.csv) no está en la carpeta; el docente debe compartirlo o cada estudiante puede crear sus propios archivos (ver sección 0). Los programas con ventana se revisaron siguiendo la lógica del código y la documentación oficial de Tkinter (se abre en otra pestaña). Ejecútalos en tu computador para ver el resultado.


Tabla de contenidos

  1. Antes de empezar: el proyecto y la idea detrás de él
  2. Paso 1: la interfaz con Tkinter
  3. Paso 2: crear tarjetas nuevas con pandas
  4. Paso 3: voltear la tarjeta con after
  5. Paso 4: guardar el progreso
  6. 🚀 Proyecto del día: Flashy completo
  7. 🌟 Proyecto extra: el Mazo de Repaso para las Pruebas Saber 11
  8. 🏆 Proyecto integrador del nivel intermedio (Días 15 a 31): la Estación de Estudio
  9. 🧩 Comprueba lo aprendido
  10. 🤖 Programar con apoyo de IA
  11. 🛠️ Correcciones técnicas al material original
  12. 📋 Matriz de trazabilidad
  13. 📦 Evidencias de aprendizaje del Día 31

0. Antes de empezar: el proyecto y la idea detrás de él

(Fuente: 234 - Day 31 Goals- what you will make by the end of the day.md)

Hoy construiremos Flashy, una aplicación de tarjetas de estudio (flash cards) para aprender vocabulario de otro idioma:

  • La tarjeta muestra una palabra en francés.
  • A los 3 segundos, la tarjeta se voltea y muestra la traducción al inglés.
  • Si ya sabías la palabra, pulsas ✅ y no vuelve a aparecer. Si no la sabías, pulsas ❌ y sigue en el mazo.
  • El progreso se guarda: la próxima vez que abras la aplicación, solo verás las palabras que aún no sabes.

La idea pedagógica: el diccionario de frecuencias

La profesora del curso cuenta que estudió francés durante años memorizando listas y tablas gramaticales, y no aprendió a hablarlo. Lo que sí le funcionó fue una idea sencilla:

  • En chino hay unos 50 000 caracteres, pero para leer textos sencillos basta con unos 1000.
  • Lo importante es aprender primero las palabras más usadas. Un diccionario de frecuencias no ordena las palabras alfabéticamente, sino por cuántas veces aparecen en el uso real.
  • Las listas que usa el curso se construyeron contando las palabras de los subtítulos de películas y series.

Para crear las tarjetas, la profesora copió 100 palabras frecuentes en una hoja de cálculo de Google y usó la fórmula =GOOGLETRANSLATE(celda, "fr", "en") para traducirlas. Luego descargó la hoja como CSV. Ese archivo, french_words.csv, ya viene en el proyecto inicial.

💡 Conexión: esta es una aplicación del análisis de datos que viste con pandas (Día 25): contar frecuencias y ordenar por ellas para decidir qué es más importante.

Materiales del proyecto

Carpeta / archivo Contenido
images/ card_front.png (frente de la tarjeta), card_back.png (reverso), right.png (✅) y wrong.png (❌)
data/french_words.csv 100 palabras con las columnas French y English
main.py Ya trae la constante BACKGROUND_COLOR con el color de fondo

Si no tienes el proyecto inicial: crea un data/french_words.csv con al menos 10 palabras (primera línea: French,English). Para las imágenes, crea en cualquier editor un PNG de 800 × 526 para cada cara de la tarjeta y dos PNG pequeños (por ejemplo, 100 × 100) para los botones. También puedes hacer las tarjetas en inglés–español: solo cambian los nombres de las columnas.

Prerequisito general: Tkinter, Canvas, PhotoImage, grid, columnspan y highlightthickness (Días 27 y 28); window.after y after_cancel (Día 28, Pomodoro); pandas, read_csv y DataFrame (Día 25); excepciones y FileNotFoundError (Día 30); variables globales (Día 12); random.choice (Día 4).


1. Paso 1: la interfaz con Tkinter

(Fuentes: 235-step-1-create-the-user-interface-ui-with-tkinter.md, 236 - Solution & Walkthrough for Creating the UI.md)

🎯 Objetivo de aprendizaje

Construir una interfaz con un Canvas que superpone una imagen y dos textos, y botones con imágenes, organizados en una cuadrícula.

📝 Ejercicio original del curso

Enunciado: usar las imágenes de la carpeta images para construir la interfaz: una tarjeta con un título y una palabra, y debajo dos botones con imagen (❌ y ✅).

Pistas del curso:

  1. Una cuadrícula de 2 × 2, donde la tarjeta ocupa 2 columnas.
  2. La tarjeta es un Canvas con 1 imagen y 2 textos.
  3. La imagen es card_front.png, creada con PhotoImage. Cuidado con la ruta: la imagen está dentro de la carpeta images.
  4. Para poner una imagen en un botón:
Python
my_image = PhotoImage(file="path/to/image_file.png")
button = Button(image=my_image, highlightthickness=0)

🔍 Antes de programar

  • Dibuja la cuadrícula: ¿en qué fila y columna va cada elemento?
  • ¿Por qué la tarjeta necesita un Canvas y no un Label? (Pista: ¿cuántas cosas van una encima de otra?)
  • La imagen de la tarjeta mide 800 × 526. ¿Cuál es el punto central?

📖 Explicación conceptual

¿Por qué un Canvas? Un Canvas (lienzo) permite superponer elementos: primero la imagen de la tarjeta y encima los dos textos. Cada elemento se ubica con coordenadas relativas al lienzo, no a la ventana.

Coordenadas en el lienzo: en Tkinter, el punto (0, 0) es la esquina superior izquierda y Y crece hacia abajo (al contrario de Turtle). Por eso el centro de un lienzo de 800 × 526 es (400, 263), y un texto en Y = 150 queda más arriba que el centro.

🔎 Ejemplo explicado (la solución del curso, reconstruida)

Python
from tkinter import *

BACKGROUND_COLOR = "#B1DDC6"   # usa el valor que trae el main.py del proyecto inicial

window = Tk()
window.title("Flashy")
window.config(padx=50, pady=50, bg=BACKGROUND_COLOR)

# La tarjeta: un lienzo del tamaño de la imagen
canvas = Canvas(width=800, height=526)
card_front_img = PhotoImage(file="images/card_front.png")
canvas.create_image(400, 263, image=card_front_img)
canvas.create_text(400, 150, text="Title", font=("Arial", 40, "italic"))
canvas.create_text(400, 263, text="word", font=("Arial", 60, "bold"))
canvas.config(bg=BACKGROUND_COLOR, highlightthickness=0)
canvas.grid(row=0, column=0, columnspan=2)

# Los botones
cross_image = PhotoImage(file="images/wrong.png")
unknown_button = Button(image=cross_image, highlightthickness=0)
unknown_button.grid(row=1, column=0)

check_image = PhotoImage(file="images/right.png")
known_button = Button(image=check_image, highlightthickness=0)
known_button.grid(row=1, column=1)

window.mainloop()

Qué hace cada parte y por qué:

  • padx=50, pady=50: deja 50 píxeles de margen alrededor.
  • create_image(400, 263, ...): la imagen se ubica por su centro, así que la ponemos en el centro del lienzo.
  • bg=BACKGROUND_COLOR: la imagen tiene esquinas redondeadas y sombra; sin esto se vería un fondo blanco detrás.
  • highlightthickness=0: quita el borde que Tkinter dibuja alrededor del lienzo y de los botones.
  • columnspan=2: el lienzo ocupa las columnas 0 y 1. Sin esto, el botón ✅ queda lejos a la derecha, porque la columna 0 se vuelve tan ancha como el lienzo. El curso muestra este error y lo corrige.
  • El tamaño del texto de la palabra (60) es un ejemplo: la lección solo dice que es "más grande y en negrita" que el título.

Consejo del curso: al escribir rutas como "images/card_front.png" es fácil cometer errores de escritura. Usa el autocompletado del editor: escribe la carpeta, / y elige el archivo.

Detalle visual: en Windows y Mac queda una línea fina alrededor de los botones aunque pongas highlightthickness=0. El curso explica que es normal: le muestra al usuario que el botón se presionó.

⚠️ Errores frecuentes

Error Qué ocurre Solución
PhotoImage(file="card_front.png") sin la carpeta TclError: couldn't open "card_front.png" Escribe la ruta completa: "images/card_front.png"
Crear un PhotoImage antes de Tk() RuntimeError: Too early to create image: no default root window (verificado) Crea la ventana primero
Olvidar columnspan=2 El botón ✅ queda desplazado a la derecha La tarjeta ocupa las dos columnas
Ejecutar main.py desde otra carpeta La ruta relativa no encuentra las imágenes Abre en el editor la carpeta del proyecto, o usa rutas con Path(__file__).parent (Día 24)
Confundir las coordenadas con las de Turtle El título aparece abajo En Tkinter, Y crece hacia abajo

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: agrega una etiqueta debajo de los botones que diga "¿Conocías esta palabra?".
  • 🟡 Reto 2: agrega un tercer texto en el lienzo, en la parte inferior de la tarjeta, que más adelante mostrará cuántas palabras faltan.
  • 🟠 Reto 3: reemplaza las rutas escritas a mano por rutas construidas con Path(__file__).parent / "images" / "card_front.png". Ejecuta el programa desde otra carpeta y comprueba que sigue funcionando.
  • 🔴 Reto 4: diseña la interfaz para una pantalla pequeña (por ejemplo, un portátil de 1366 × 768). ¿Cabe la ventana? ¿Qué cambiarías: el tamaño del lienzo, las imágenes o el margen? Justifica tu decisión.

2. Paso 2: crear tarjetas nuevas con pandas

(Fuentes: 237-step-2-create-new-flash-cards.md, 238 - Solution & Walkthrough for Creating New Flash Cards.md)

🎯 Objetivo de aprendizaje

  • Convertir un CSV en una lista de diccionarios con pandas.
  • Elegir un elemento al azar y mostrarlo en el lienzo con itemconfig.

📝 Ejercicio original del curso

Enunciado:

  1. Leer los datos de data/french_words.csv.
  2. Elegir una palabra francesa al azar y mostrarla en la tarjeta. Cada vez que se pulse ❌ o ✅, debe aparecer una palabra nueva al azar.

Pista del curso: usar pandas para crear un DataFrame y convertirlo en una lista de diccionarios con DataFrame.to_dict(orient="records").

🔍 Antes de programar

  • ¿Qué forma tienen los datos del CSV? Abre el archivo y mira la primera línea.
  • ¿Qué estructura es más cómoda para elegir "una fila al azar": un diccionario de columnas o una lista de filas?
  • ¿Cómo cambias el texto de un elemento que ya existe en el lienzo?

📖 Explicación conceptual: las formas de to_dict

Con un CSV de tres palabras:

Texto
French,English
partie,part
histoire,history
chaque,each

Forma por defecto (organizada por columnas):

Python
data.to_dict()
# {'French': {0: 'partie', 1: 'histoire', 2: 'chaque'},
#  'English': {0: 'part', 1: 'history', 2: 'each'}}

Con orient="records" (organizada por filas, una lista de diccionarios):

Python
data.to_dict(orient="records")
# [{'French': 'partie', 'English': 'part'},
#  {'French': 'histoire', 'English': 'history'},
#  {'French': 'chaque', 'English': 'each'}]

(Ambos resultados verificados con pandas 3.0.)

La segunda forma es perfecta para tarjetas: cada diccionario es una tarjeta, con su frente ("French") y su reverso ("English"). Así, random.choice elige una tarjeta completa.

🛠️ Nota técnica: la pista de la lección 237 dibuja el resultado como [{french_word: english_word}, ...], es decir, con la palabra francesa como clave. No es así: las claves son los nombres de las columnas, "French" y "English", como muestra el resultado verificado. La solución en video (238) sí lo usa correctamente: current_card["French"].

Cambiar un elemento del lienzo: create_text devuelve un identificador. Si lo guardas en una variable, después puedes modificarlo:

Python
card_title = canvas.create_text(400, 150, text="", font=("Arial", 40, "italic"))
# ... más adelante:
canvas.itemconfig(card_title, text="French")

🔎 La solución del curso (estructura)

Python
import random
import pandas

data = pandas.read_csv("data/french_words.csv")
to_learn = data.to_dict(orient="records")

def next_card():
    current_card = random.choice(to_learn)
    canvas.itemconfig(card_title, text="French")
    canvas.itemconfig(card_word, text=current_card["French"])
  • Los dos botones reciben command=next_card (sin paréntesis: se pasa la función, no su resultado).
  • Se llama next_card() una vez después de crear la interfaz y antes de mainloop(), para que la primera tarjeta no muestre los textos de prueba "Title" y "word".
  • Las claves distinguen mayúsculas: el CSV dice French, con F mayúscula.

⚠️ Errores frecuentes

Error Qué ocurre Solución
orient="record" (sin la s) ValueError: orient 'record' not understood (verificado) Escribe "records"
current_card["french"] KeyError: 'french' Usa exactamente el nombre de la columna: "French"
command=next_card() La función se ejecuta una vez al crear el botón y el botón no hace nada Quita los paréntesis
No llamar next_card() antes de mainloop La primera tarjeta dice "Title" y "word" Llama la función al final de la interfaz
ModuleNotFoundError: No module named 'pandas' pandas no está instalado en el intérprete del proyecto Instálalo desde el editor o con pip install pandas

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: imprime len(to_learn) al iniciar. ¿Coincide con el número de filas del CSV sin contar el encabezado?
  • 🟡 Reto 2: evita que la misma palabra salga dos veces seguidas.
  • 🟠 Reto 3: crea tu propio CSV de 20 palabras inglés–español con vocabulario de tu clase de inglés y haz que la aplicación funcione con él. ¿Qué tuviste que cambiar en el código? ¿Podrías cambiarlo en un solo lugar?
  • 🔴 Reto 4: en lugar de elegir totalmente al azar, haz que las palabras aparezcan en el orden del diccionario de frecuencias (primero las más usadas). ¿Qué ventaja y qué desventaja tiene frente al azar?

3. Paso 3: voltear la tarjeta con after

(Fuentes: 239-step-3-flip-the-cards.md, 240 - Solution & Walkthrough for Flipping Cards.md)

🎯 Objetivo de aprendizaje

  • Programar una acción retrasada sin bloquear la interfaz, usando window.after.
  • Cancelar un temporizador pendiente con after_cancel.
  • Cambiar la imagen y el color de los elementos de un lienzo.

📝 Ejercicio original del curso

Enunciado:

  1. A los 3 segundos (3000 ms), la tarjeta se voltea y muestra la traducción al inglés de la palabra actual.
  2. La imagen cambia a card_back.png, el texto se vuelve blanco y el título pasa de "French" a "English".

Pistas del curso:

  1. Para cambiar la imagen del lienzo necesitas guardar la referencia que devuelve create_image y luego usar itemconfig(..., image=nueva_imagen).
  2. Importante: los objetos PhotoImage no deben crearse dentro de una función.
  3. El color del texto de un lienzo se cambia con el parámetro fill.
  4. Dentro de mainloop() no uses time.sleep(); usa window.after().
  5. Un window.after() se puede cancelar con window.after_cancel().

🔍 Antes de programar

  • flip_card necesita saber qué tarjeta está en pantalla. ¿Dónde está guardada esa información ahora? ¿Puede verla otra función?
  • ¿Qué debe cambiar cuando se voltea la tarjeta? ¿Y qué debe volver a su estado inicial cuando aparece la siguiente?
  • Si el usuario pulsa ✅ muy rápido cinco veces, ¿cuántos temporizadores quedan "contando"?

📖 Explicación conceptual

¿Por qué no time.sleep(3)? mainloop() es un bucle que atiende constantemente la ventana: clics, dibujos, teclado. sleep congela ese bucle: la ventana no responde durante 3 segundos. window.after(3000, flip_card) en cambio le dice a Tkinter: "dentro de 3000 ms, llama a flip_card", y mientras tanto la ventana sigue funcionando. Ya lo usaste en el Pomodoro (Día 28).

after devuelve un identificador. Si lo guardas, puedes cancelarlo:

Python
flip_timer = window.after(3000, func=flip_card)
window.after_cancel(flip_timer)

🔎 La solución del curso, construida por partes

Parte 1 — Compartir la tarjeta actual. current_card estaba "encerrada" dentro de next_card. El curso la saca como variable global que empieza como diccionario vacío:

Python
current_card = {}

def next_card():
    global current_card
    current_card = random.choice(to_learn)
    ...

def flip_card():
    canvas.itemconfig(card_title, text="English", fill="white")
    canvas.itemconfig(card_word, text=current_card["English"], fill="white")
    canvas.itemconfig(card_background, image=card_back_img)

Parte 2 — Cambiar la imagen. Se guarda la referencia de la imagen del lienzo (card_background = canvas.create_image(...)) y se crea card_back_img fuera de las funciones, junto a card_front_img.

Parte 3 — Devolver la tarjeta al frente. En next_card hay que deshacer el volteo: título "French", texto negro (fill="black") e imagen card_front_img.

Parte 4 — Repetir el volteo en cada tarjeta. Si window.after solo se llama al crear la ventana, la primera tarjeta se voltea, pero las siguientes no. Por eso next_card también programa el volteo.

Parte 5 — El error de los temporizadores acumulados. El curso lo muestra: si pulsas los botones muchas veces, la tarjeta se voltea de inmediato. ¿Por qué? Cada after sigue contando en segundo plano, sin importar que ya cambiaste de tarjeta. La solución es cancelar el temporizador anterior antes de crear uno nuevo:

Python
flip_timer = window.after(3000, func=flip_card)   # justo después de crear la ventana

def next_card():
    global current_card, flip_timer
    window.after_cancel(flip_timer)
    current_card = random.choice(to_learn)
    # ... volver al frente y mostrar la palabra ...
    flip_timer = window.after(3000, func=flip_card)

🛠️ Nota técnica: la pista dice que un PhotoImage "no debe crearse dentro de una función, o no funcionará". La causa real es que, al terminar la función, la variable local desaparece y Python libera la imagen (recolección de basura); Tkinter se queda mostrando un espacio vacío. Se puede crear dentro de una función si se conserva una referencia (por ejemplo, en una variable global o en un atributo de un objeto). La regla del curso es la forma más sencilla de evitar el problema.

⚠️ Errores frecuentes

Error Qué ocurre Solución
Usar time.sleep(3) La ventana se congela Usa window.after
Olvidar global current_card en next_card flip_card muestra la tarjeta vacía del inicio: KeyError: 'English' Declara la variable como global
Crear flip_timer después de la primera llamada a next_card() NameError al hacer after_cancel(flip_timer) Crea el primer temporizador justo después de la ventana
No volver a fill="black" en next_card El texto blanco no se ve sobre el frente blanco Restaura el color y la imagen del frente
No cancelar el temporizador La tarjeta se voltea antes de tiempo window.after_cancel(flip_timer)

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: convierte los 3000 ms en una constante FLIP_DELAY_MS y prueba con 1500 y con 5000. ¿Cuál es mejor para estudiar?
  • 🟡 Reto 2: permite voltear la tarjeta manualmente haciendo clic sobre ella (investiga canvas.bind("<Button-1>", ...)), además del volteo automático.
  • 🟠 Reto 3: muestra una cuenta regresiva (3, 2, 1) en una esquina de la tarjeta antes de voltear, usando after cada segundo como en el Pomodoro.
  • 🔴 Reto 4: reescribe la aplicación con una clase FlashyApp en la que current_card, flip_timer y las imágenes sean atributos, sin ninguna variable global. ¿Qué ventajas tiene? (En el Día 34 el curso construye una interfaz Tkinter con clases.)

4. Paso 4: guardar el progreso

(Fuentes: 241-step-4-save-your-progress.md, 242 - Solution & Walkthrough for Saving Progress.md)

🎯 Objetivo de aprendizaje

  • Eliminar elementos de una lista y guardar la lista como CSV.
  • Elegir entre dos fuentes de datos según exista o no un archivo, usando try/except/else.

📝 Ejercicio original del curso

Enunciado:

  1. Si el usuario pulsa ✅, la palabra actual se elimina de la lista de palabras que pueden salir.
  2. La lista actualizada se guarda en un archivo nuevo: words_to_learn.csv.
  3. Al iniciar, el programa revisa si existe words_to_learn.csv. Si existe, usa esas palabras; si no (la primera vez), usa french_words.csv.

Ejemplo del curso: si french_words.csv tuviera chaque,each, parlé,speak y arrivé,come, y el usuario marca que conoce parlé, words_to_learn.csv quedaría solo con chaque y arrivé.

Pistas del curso: list.remove(), DataFrame.to_csv() y el parámetro index=False.

🔍 Antes de programar

  • ¿Qué hace ❌ ahora y qué debe hacer ✅? ¿Siguen llamando a la misma función?
  • ¿En qué momento conviene guardar el archivo: al cerrar la aplicación o cada vez que se pulsa ✅? ¿Qué pasa si la aplicación se cierra de forma inesperada?
  • ¿Qué línea puede fallar la primera vez que se ejecuta el programa?

🔎 La solución del curso, paso a paso

1. Una función nueva para ✅. El botón ❌ sigue con next_card; el ✅ ahora llama a is_known:

Python
def is_known():
    to_learn.remove(current_card)
    data = pandas.DataFrame(to_learn)
    data.to_csv("data/words_to_learn.csv", index=False)
    next_card()

remove busca en la lista un diccionario igual a current_card y elimina el primero que encuentra (verificado).

2. Elegir la fuente de datos. Es el patrón del Día 30:

Python
try:
    data = pandas.read_csv("data/words_to_learn.csv")
except FileNotFoundError:
    original_data = pandas.read_csv("data/french_words.csv")
    to_learn = original_data.to_dict(orient="records")
else:
    to_learn = data.to_dict(orient="records")

3. Guardar junto a los otros datos. La primera versión del curso guardaba words_to_learn.csv en la carpeta principal; luego lo movió a data/ para mantener el proyecto ordenado.

📖 El error de las columnas que se multiplican

El curso encontró un error "extraño": cada vez que se ejecutaba el programa, words_to_learn.csv tenía una columna más. Así se ve, paso a paso (verificado):

Ejecución Contenido de words_to_learn.csv
1.ª (con to_csv sin index=False) ,French,English / 0,partie,part / …
2.ª (lee el archivo anterior y lo vuelve a guardar) ,Unnamed: 0,French,English / 0,0,partie,part / …

pandas guarda por defecto el índice (los números de fila) como primera columna. Al leer, esa columna se convierte en un dato más (Unnamed: 0), y al guardar se agrega otro índice. La solución: to_csv(..., index=False).

🛠️ Nota técnica: la pista 2 de la lección 241 habla de crear un CSV "a partir de un diccionario". En realidad, to_learn es una lista de diccionarios; pandas.DataFrame(to_learn) la convierte en tabla (una fila por diccionario).

⚠️ Errores frecuentes

Error Qué ocurre Solución
Olvidar index=False Columnas Unnamed: 0 que crecen en cada ejecución Agrega index=False
Conectar ✅ con next_card en vez de is_known Las palabras nunca se eliminan Revisa el command de cada botón
Leer siempre french_words.csv El progreso se pierde al reiniciar Usa el try/except/else
Guardar sobre french_words.csv Se pierden para siempre las palabras originales Guarda en un archivo distinto

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: muestra en la ventana cuántas palabras quedan por aprender y actualízalo al pulsar ✅.
  • 🟡 Reto 2: agrega un botón Reiniciar que borre words_to_learn.csv (con confirmación, messagebox.askyesno) para empezar de cero. (Pista: Path.unlink() del Día 24.)
  • 🟠 Reto 3: además de las palabras que faltan, guarda en data/learned.csv las que ya aprendiste, con la fecha en que las marcaste. (Pista: datetime, que se estudia a fondo el Día 32.)
  • 🔴 Reto 4: ahora mismo, pulsar ❌ no cambia nada. Diseña un sistema en el que las palabras que fallas aparezcan más seguido (por ejemplo, contando fallos por palabra). ¿Qué columna nueva necesitaría el CSV?

5. 🚀 Proyecto del día: Flashy completo

(Fuentes: 234 a 242)

Problema

Integrar los cuatro pasos en una aplicación de estudio funcional, probada y robusta.

Contexto

El curso cierra con esta idea: la aplicación sirve para cualquier cosa que se estudie con "frente y reverso": idiomas, términos de programación, historia o geografía. Tú decides qué mazo construir.

Requisitos funcionales

  1. Tarjeta con título e idioma al frente; a los 3 segundos se voltea y muestra la traducción.
  2. ❌ muestra otra palabra; ✅ la elimina del mazo y muestra otra.
  3. El temporizador se reinicia con cada tarjeta nueva.
  4. El progreso se guarda en data/words_to_learn.csv sin columnas de índice.
  5. La primera ejecución usa french_words.csv sin errores.

Restricciones técnicas

Tkinter con Canvas y grid; pandas para leer y guardar; window.after (nunca sleep); try/except/else para elegir el archivo; todos los PhotoImage fuera de las funciones.

Planificación

  1. Entradas: clics en ❌ y ✅, y los archivos CSV.
  2. Estado: to_learn, current_card, flip_timer.
  3. Salidas: la tarjeta en pantalla y words_to_learn.csv.
  4. Construye en el orden del curso: interfaz → tarjetas → volteo → progreso. Ejecuta después de cada paso.

Pruebas

Caso Resultado esperado
Primera ejecución (sin words_to_learn.csv) Aparece una palabra en francés; no hay errores
Esperar 3 segundos Tarjeta verde, título "English", texto blanco
Pulsar ❌ con la tarjeta volteada Vuelve el frente, texto negro, palabra nueva
Pulsar ✅ cinco veces rápido La tarjeta no se voltea antes de 3 s
Pulsar ✅ Se crea o actualiza words_to_learn.csv con una fila menos
Cerrar y abrir El número de palabras continúa donde quedó
Abrir words_to_learn.csv Solo dos columnas: French,English
Marcar ✅ en todas las palabras ⚠️ (ver 🧪)

🧪 Prueba y depuración: ¿qué pasa cuando aprendes todo?

Usa un CSV con 3 palabras para probar rápido.

  1. Durante la sesión. Marca ✅ tres veces. Al eliminar la última, next_card() ejecuta random.choice([]) y aparece IndexError: Cannot choose from an empty sequence (verificado). La ventana sigue abierta pero los botones ya no funcionan.
  2. En la siguiente ejecución. Guardar una lista vacía produce un archivo que solo contiene un salto de línea. Al leerlo, pandas lanza EmptyDataError: No columns to parse from file (verificado), y el except FileNotFoundError del curso no lo captura: la aplicación ni siquiera abre.

Actividad:

  • Reproduce los dos errores y copia los mensajes en tu cuaderno.
  • Decide qué debería pasar cuando el usuario aprende todas las palabras (¿un mensaje de felicitación?, ¿reiniciar el mazo?, ¿desactivar los botones?) e impleméntalo.
  • Protege la lectura del archivo para que un words_to_learn.csv vacío o dañado no impida abrir la aplicación. (Pista: pandas.errors.EmptyDataError.)

⭐ Extensión del proyecto

  • Contador de palabras aprendidas y restantes.
  • Teclas rápidas: ← para ❌ y → para ✅ (window.bind).
  • Selector de mazo al iniciar (francés, inglés, términos de programación).
  • Estadística al cerrar: cuántas palabras aprendiste en la sesión.

6. 🌟 Proyecto extra: el Mazo de Repaso para las Pruebas Saber 11

Situación

Los estudiantes de grado 11 del colegio se preparan para las Pruebas Saber 11. El grupo de décimo quiere dejarles una herramienta: una aplicación de tarjetas con varios mazos (por ejemplo, Ciencias Naturales, Sociales y Ciudadanas, e Inglés), escritos por los propios estudiantes.

Requisitos

  1. Un CSV por mazo, con las columnas Pregunta,Respuesta (mínimo 15 tarjetas por mazo, redactadas y revisadas por el grupo).
  2. Al iniciar, el usuario elige el mazo (con botones o un menú).
  3. Voltear con retraso, ❌ y ✅ como en Flashy.
  4. El progreso se guarda por mazo (un archivo de pendientes para cada uno).
  5. Un archivo estadisticas.json guarda, por mazo, cuántas veces se respondió cada tarjeta con ❌ y con ✅.
  6. Al terminar un mazo, un mensaje de felicitación y la opción de reiniciarlo.
  7. Ningún archivo faltante, vacío o dañado debe cerrar la aplicación.

Decisiones que debes tomar

  • Las preguntas pueden ser más largas que una palabra. ¿Cómo haces que el texto quepa en la tarjeta? (Investiga el parámetro width de create_text.)
  • ¿El retraso para voltear debe ser igual para todos los mazos?
  • ¿Guardas las estadísticas en JSON o en CSV? Justifica.

Casos de prueba

Caso Esperado
Elegir un mazo por primera vez Se carga el CSV original del mazo
Aprender 3 tarjetas, cerrar y volver a ese mazo Faltan 3 tarjetas menos; los otros mazos no cambian
Borrar estadisticas.json La aplicación lo crea de nuevo sin errores
Terminar un mazo Mensaje de felicitación, sin IndexError

7. 🏆 Proyecto integrador del nivel intermedio (Días 15 a 31): la Estación de Estudio

Proyecto integrador creado para esta adaptación. Cierra el nivel intermedio del curso: programación orientada a objetos (Días 16 y 17), archivos y rutas (Día 24), CSV y pandas (Día 25), comprensión de listas y diccionarios (Día 26), Tkinter (Días 27 a 29), excepciones y JSON (Día 30) y el proyecto Flashy (Día 31).

Situación problema

Muchos estudiantes dicen que "estudian", pero no saben cuánto ni qué estudiaron. El comité académico del colegio propone una aplicación de escritorio que combine un temporizador Pomodoro con tarjetas de estudio y lleve un registro del tiempo y del progreso.

Pregunta orientadora

¿Cómo integrar en una sola aplicación bien organizada lo que aprendimos sobre clases, archivos, datos, interfaces gráficas y manejo de errores, para ayudar a un estudiante a estudiar mejor?

Objetivo

Construir la Estación de Estudio: una aplicación Tkinter que alterna bloques de estudio con tarjetas y descansos, y guarda estadísticas de cada sesión.

Requerimientos

  1. Clases propias (mínimo 3), por ejemplo: Tarjeta, Mazo (carga, elimina y guarda tarjetas) y EstacionApp (la interfaz). Ninguna variable global.
  2. Pomodoro: bloques de estudio y descanso con after; durante el estudio se muestran tarjetas; durante el descanso, los botones se desactivan.
  3. Mazos en CSV leídos con pandas; el progreso de cada mazo se guarda sin columnas de índice.
  4. Estadísticas en JSON: por fecha, minutos estudiados y tarjetas marcadas con ✅ y ❌.
  5. Al menos una comprensión de listas o diccionarios (por ejemplo, para calcular el porcentaje de aciertos por mazo).
  6. Rutas construidas con pathlib (Día 24).
  7. Manejo de FileNotFoundError, EmptyDataError y JSONDecodeError: la aplicación nunca se cierra por un archivo.
  8. Una ventana de resumen con las estadísticas de los últimos 7 días.

Restricciones

  • Solo bibliotecas vistas en el curso (Tkinter, pandas, json, random, pathlib, datetime básico).
  • Todos los valores configurables (minutos, retraso de volteo, colores) como constantes en mayúsculas.

Etapas de desarrollo

Etapa Entregable parcial
1. Diseño Boceto de la interfaz y diagrama de clases
2. Datos Clase Mazo probada sin interfaz (desde la consola)
3. Interfaz Tarjetas funcionando dentro de la clase EstacionApp
4. Pomodoro Alternancia de estudio y descanso
5. Persistencia Progreso en CSV y estadísticas en JSON
6. Robustez Todos los casos de archivos faltantes o dañados
7. Presentación Demostración y explicación del código a otro grupo

Entregables

  • Código en varios archivos (una clase por archivo como mínimo para Mazo y EstacionApp).
  • Diagrama de clases y boceto de la interfaz.
  • Al menos 12 casos de prueba con resultados.
  • Registro de errores con al menos 6 errores reales y su solución.
  • Video corto de una sesión de estudio.

Casos de prueba mínimos

Caso Esperado
Primera ejecución sin archivos de progreso ni estadísticas Se crean sin errores
estadisticas.json vacío La aplicación abre y lo reinicia
Terminar un mazo Mensaje; sin IndexError
Cerrar en mitad de un bloque y volver a abrir El progreso de tarjetas se conserva
Cambiar la duración del bloque en una constante El temporizador cambia sin modificar métodos

Criterios técnicos de funcionamiento

  • La clase Mazo se puede usar y probar sin la interfaz.
  • No hay código repetido para cargar o guardar archivos.
  • Ninguna acción del usuario congela la ventana (no hay sleep).
  • Otro estudiante puede agregar un mazo nuevo sin tocar el código, solo agregando un CSV.

Posibles extensiones

Gráfica de progreso semanal (con lo que se verá en días posteriores), sonido al terminar un bloque, envío del resumen por correo (Día 32).


8. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Por qué la tarjeta usa un Canvas y no un Label?
  2. ¿Qué diferencia hay entre data.to_dict() y data.to_dict(orient="records")? ¿Cuál conviene para elegir una tarjeta al azar y por qué?
  3. ¿Por qué no se puede usar time.sleep(3) para voltear la tarjeta?
  4. ¿Qué es un "diccionario de frecuencias" y por qué ayuda a aprender un idioma?

Análisis de código

Python
try:
    data = pandas.read_csv("data/words_to_learn.csv")
except FileNotFoundError:
    original_data = pandas.read_csv("data/french_words.csv")
    to_learn = original_data.to_dict(orient="records")
else:
    to_learn = data.to_dict(orient="records")

¿Qué bloque se ejecuta la primera vez que se abre la aplicación? ¿Y la segunda? ¿Qué pasaría si se eliminara el else y su contenido?

Predicción

  1. Un usuario pulsa ✅ cuatro veces seguidas en menos de un segundo sin after_cancel. ¿Cuántas veces se ejecutará flip_card en los siguientes 3 segundos?
  2. Un CSV con 100 palabras se guarda tres veces seguidas sin index=False (leyendo el anterior cada vez). ¿Cuántas columnas tendrá el archivo final?

Depuración

Python
def next_card():
    current_card = random.choice(to_learn)
    canvas.itemconfig(card_word, text=current_card["French"])

def flip_card():
    canvas.itemconfig(card_word, text=current_card["English"])

Con current_card = {} definido arriba, la tarjeta se voltea y el programa muestra un error. ¿Cuál? ¿Por qué? ¿Cómo se corrige?

Python
def flip_card():
    card_back_img = PhotoImage(file="images/card_back.png")
    canvas.itemconfig(card_background, image=card_back_img)

Al voltear, la tarjeta queda vacía. ¿Por qué?

Modificación

Cambia la aplicación para que la tarjeta muestre primero el inglés y al voltear el francés. ¿Cuántos lugares del código tuviste que modificar? ¿Cómo podrías lograrlo cambiando una sola constante?

Creación

Escribe una función porcentaje_aprendido(total_original, pendientes) que devuelva el porcentaje de palabras aprendidas redondeado a un decimal, y úsala para mostrar el progreso en la ventana.


9. 🤖 Programar con apoyo de IA

Actividad 1: crear el mazo con IA, verificarlo con criterio. La profesora usó Google Translate para traducir 100 palabras de una vez. Una IA también puede generar un CSV de vocabulario en segundos. Pero:

  1. Pide a una IA 20 palabras frecuentes en inglés con su traducción al español, en formato CSV.
  2. Revisa cada traducción con un diccionario confiable o con tu profesor de inglés. ¿Hay palabras con varios significados donde la IA eligió solo uno? ¿Hay errores?
  3. Comprueba que el CSV abre con pandas y que los nombres de las columnas son los que tu programa espera.
  4. Anota cuántas correcciones hiciste. ¿Confiarías en un mazo sin revisar para estudiar para un examen?

Actividad 2: comprobar una explicación técnica. Pregunta a una IA: "¿Por qué una imagen de Tkinter creada dentro de una función no se muestra?". Compara su respuesta con la nota técnica del Paso 3 y con la regla del curso. ¿Menciona la recolección de basura? ¿Propone guardar una referencia?

Actividad 3: la IA como probadora. Pega tu código final y pide: "Dame 5 casos de prueba que podrían romper esta aplicación". ¿Encuentra el caso de "aprender todas las palabras"? Ejecuta cada caso que proponga y registra si realmente rompe tu programa.


10. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
235 Enlaces a effbot.org para Canvas y PhotoImage Ese sitio ya no publica la documentación de Tkinter; usa la documentación oficial (se abre en otra pestaña) o TkDocs (se abre en otra pestaña) Sección 1
237 El resultado de to_dict(orient="records") se dibuja como [{french_word: english_word}] Las claves son los nombres de las columnas: [{'French': ..., 'English': ...}] (verificado) Nota técnica de la sección 2
239 "Los PhotoImage no deben crearse dentro de una función, o no funcionará" La causa es la recolección de basura: funciona si se conserva una referencia Nota técnica de la sección 3
241 Crear un CSV "a partir de un diccionario" to_learn es una lista de diccionarios Nota técnica de la sección 4
242 Solo se captura FileNotFoundError Un words_to_learn.csv vacío produce EmptyDataError sin capturar (verificado) 🧪 del proyecto
242 next_card usa random.choice(to_learn) Al aprender la última palabra, IndexError por lista vacía (verificado) 🧪 del proyecto
236 Rutas relativas como "images/card_front.png" Dependen de la carpeta desde la que se ejecuta el programa Errores frecuentes de la sección 1; Reto 3

11. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Objetivos, diccionario de frecuencias, GOOGLETRANSLATE, CSV 234 Sección 0 — Actividad 1 de IA Secciones 5 y 6
Interfaz: cuadrícula 2 × 2, Canvas, PhotoImage, botones con imagen 235 Sección 1 Paso 1 Retos 1–4 de la sección 1 Secciones 5, 6 y 7
Solución de la interfaz: columnspan, highlightthickness, rutas 236 Sección 1 Paso 1 Retos 1–4 de la sección 1 Secciones 5, 6 y 7
read_csv, to_dict(orient="records"), random.choice 237 Sección 2 Paso 2 Retos 1–4 de la sección 2 Secciones 5, 6 y 7
next_card, itemconfig, llamar antes de mainloop 238 Sección 2 Paso 2 Retos 1–4 de la sección 2 Secciones 5, 6 y 7
after, fill, PhotoImage fuera de funciones, after_cancel 239 Sección 3 Paso 3 Retos 1–4 de la sección 3 Secciones 5, 6 y 7
global current_card, cambiar imagen, flip_timer 240 Sección 3 Paso 3 Retos 1–4 de la sección 3 Secciones 5, 6 y 7
remove, to_csv, index=False, archivo de progreso 241 Sección 4 Paso 4 Retos 1–4 de la sección 4 Secciones 5, 6 y 7
is_known, try/except/else, columnas Unnamed: 0 242 Sección 4 Paso 4 🧪 del proyecto Secciones 5, 6 y 7
(No existe en el curso original) — — — Mazo de Repaso Saber 11 Sección 6
(No existe en el curso original) — — — Estación de Estudio (Días 15–31) Sección 7

12. 📦 Evidencias de aprendizaje del Día 31

  • El boceto de la cuadrícula y la interfaz funcionando (captura).
  • El resultado impreso de to_dict() y de to_dict(orient="records") con tu CSV, y tu explicación de la diferencia.
  • Un video corto o capturas que muestren el volteo y la prueba de pulsar ✅ rápido varias veces.
  • words_to_learn.csv antes y después de marcar varias palabras (sin columnas de índice).
  • La solución a los dos errores de la sección 🧪 (aprender todas las palabras).
  • Tu propio mazo (inglés–español, Saber 11 u otro tema) con las traducciones revisadas.
  • Cuaderno de errores: rutas de imágenes, columnspan olvidado, orient="record", KeyError por mayúsculas, global olvidado, imagen que desaparece dentro de una función, temporizadores acumulados, columnas Unnamed: 0, IndexError con lista vacía y EmptyDataError.