03 — Estructuras de datos: cuando una variable no basta¶
Por qué importa¶
En el documento anterior una variable guardaba un valor: un nombre, un HP, un booleano. Pero un combate Pokémon está lleno de colecciones de valores. Charmander no tiene un movimiento, tiene una lista de hasta cuatro. La Pokédex no guarda un Pokémon, guarda diez, y necesitas buscar cualquiera por su nombre. La tabla de tipos relaciona cada tipo atacante con cómo afecta a cada tipo defensor: una rejilla entera de multiplicadores.
Si solo conocieras variables sueltas tendrías que escribir move1, move2, move3, move4 y duplicar código sin parar. Las estructuras de datos resuelven esto: te dejan guardar muchos valores juntos bajo un solo nombre y acceder a ellos de forma ordenada. Elegir la estructura correcta —lista, diccionario, tupla o conjunto— es la mitad de un buen diseño. La otra mitad es saber recorrerlas, que es el documento 04.
Este documento explica las cuatro estructuras que aparecen en pokemon/data.py (la Pokédex) y pokemon/types.py (la tabla de tipos).
Schema / Modelo mental¶
Las dos estructuras centrales del proyecto son lista y diccionario. La diferencia clave: cómo encuentras un elemento.
LISTA ── orden por POSICIÓN (índice numérico, empieza en 0)
┌───────┬────────┬────────────┬───────────┐
│ 0 │ 1 │ 2 │ 3 │ índice
├───────┼────────┼────────────┼───────────┤
│ Ember │ Scratch│ Tail Whip │ Growl │ valor
└───────┴────────┴────────────┴───────────┘
moves[0] == "Ember" moves[2] == "Tail Whip"
DICCIONARIO ── orden por CLAVE (la clave la eliges tú, suele ser texto)
┌─────────────┬──────────────────────────────────┐
│ clave │ valor │
├─────────────┼──────────────────────────────────┤
│ "Pikachu" │ {hp: 35, attack: 55, ...} │
│ "Charmander"│ {hp: 39, attack: 52, ...} │
│ "Squirtle" │ {hp: 44, attack: 48, ...} │
└─────────────┴──────────────────────────────────┘
POKEDEX["Pikachu"] ──► los datos de Pikachu
Reglas mentales:
- Lista = secuencia ordenada que indexas por número. Útil cuando el orden importa o cuando recorres todo (los movimientos de un Pokémon, el equipo).
- Diccionario = mapa de clave → valor. Útil cuando quieres buscar algo por su nombre sin recorrer todo (la Pokédex por nombre, la tabla de tipos por tipo).
- Tupla = lista que no cambia. Para agrupar cosas que van juntas y no deben modificarse, como el par
(attacker, defender)de un turno. - Conjunto = bolsa sin duplicados ni orden. Aparece poco aquí; sirve para "¿qué tipos distintos hay en el equipo?".
list — secuencia ordenada¶
Una lista guarda varios valores en orden, entre corchetes [], separados por comas. Los movimientos de un Pokémon son una lista; el equipo del jugador es una lista de Pokémon.
Indexado: acceder por posición¶
Cada elemento tiene un número de posición llamado índice, y empieza en 0, no en 1. Esto descoloca a todo principiante: el primer movimiento es moves[0], no moves[1].
moves[0] # "Ember" (el primero)
moves[1] # "Scratch"
moves[3] # "Growl" (el cuarto y último)
moves[-1] # "Growl" (índice negativo: cuenta desde el final)
len: cuántos elementos hay¶
Esto importa para validar: un Pokémon no puede tener más de 4 movimientos, y la batalla termina cuando un equipo se queda sin Pokémon en pie.
append: añadir al final¶
moves = ["Ember"]
moves.append("Scratch")
moves.append("Growl")
# moves ahora es ["Ember", "Scratch", "Growl"]
append modifica la lista existente; no devuelve una nueva. Así se construye la lista de movimientos de un Pokémon al crearlo.
in: ¿está este valor?¶
Devuelve un bool, así que encaja directamente en un if (documento 04).
Iterar: recorrer todos¶
Esto es exactamente lo que hace la interfaz cuando lista los movimientos disponibles para que el jugador elija. El detalle del for está en el documento 04; aquí basta saber que una lista se recorre de principio a fin en orden.
dict — mapa de clave a valor¶
Un diccionario asocia cada clave con un valor. Se escribe entre llaves {}, con pares clave: valor separados por comas. Es la estructura de la Pokédex: la clave es el nombre del Pokémon, el valor son sus datos.
POKEDEX = {
"Charmander": {"type": "Fire", "hp": 39, "attack": 52, "defense": 43},
"Squirtle": {"type": "Water", "hp": 44, "attack": 48, "defense": 65},
"Pikachu": {"type": "Electric", "hp": 35, "attack": 55, "defense": 40},
}
Acceso por clave¶
En vez de un número de posición, usas la clave entre corchetes:
POKEDEX["Charmander"] # {"type": "Fire", "hp": 39, ...}
POKEDEX["Charmander"]["hp"] # 39
POKEDEX["Charmander"]["type"] # "Fire"
Esto es buscar por nombre sin recorrer nada: vas directo. Por eso data.py usa un diccionario para la Pokédex y no una lista — el juego pregunta "dame los datos de Pikachu", no "dame el quinto Pokémon".
in sobre claves y acceso seguro¶
"Pikachu" in POKEDEX # True (¿existe esa clave?)
"Mewtwo" in POKEDEX # False
POKEDEX.get("Mewtwo") # None (no existe, pero no rompe)
POKEDEX["Mewtwo"] con una clave que no existe lanza un error y detiene el programa. .get(...) devuelve None en su lugar, útil cuando no estás seguro de que la clave exista.
Diccionario de diccionarios: la tabla de tipos¶
La estructura más rica del proyecto está en pokemon/types.py. TYPE_CHART es un diccionario cuyos valores son a su vez diccionarios. La clave externa es el tipo atacante; la clave interna es el tipo defensor; el valor es el multiplicador de daño.
TYPE_CHART = {
"fire": {"grass": 2.0, "water": 0.5, "fire": 0.5},
"water": {"fire": 2.0, "grass": 0.5, "water": 0.5},
"grass": {"water": 2.0, "fire": 0.5, "grass": 0.5},
# ...
}
Para saber cuánto daño hace Fuego contra Planta, encadenas dos accesos por clave:
TYPE_CHART["fire"]["grass"] # 2.0 -> súper efectivo
TYPE_CHART["fire"]["water"] # 0.5 -> poco efectivo
Visualmente, lo segundo es entrar en la fila "fire" y luego en la columna "grass":
defensor
fire water grass
attacker ┌───────────────────────┐
fire │ 0.5 0.5 2.0 │ ◄── TYPE_CHART["fire"] es esta fila
water │ 2.0 0.5 0.5 │
grass │ 0.5 2.0 0.5 │
└───────────────────────┘
▲
TYPE_CHART["fire"]["grass"] == 2.0
Cuando un par tipo-atacante / tipo-defensor no está en la tabla (combinaciones neutras), el multiplicador por defecto es 1.0. La función effectiveness() del documento 05 encapsula exactamente esa lógica: mira la tabla, y si no encuentra la combinación, devuelve 1.0.
tuple — agrupación fija¶
Una tupla es como una lista pero no se puede modificar después de crearla. Se escribe con paréntesis (). Sirve para agrupar cosas que van juntas y forman una unidad, como los dos participantes de un turno:
turn = ("Charmander", "Squirtle") # (atacante, defensor)
attacker = turn[0] # "Charmander"
defender = turn[1] # "Squirtle"
Se indexa igual que una lista (turn[0]), pero turn[0] = "Pikachu" daría error: una tupla es inmutable. Esa inmutabilidad es una garantía: si pasas un par (attacker, defender) por el código, nadie puede cambiarlo a medias por accidente.
set — colección sin duplicados (mención)¶
Un conjunto guarda valores sin orden y sin repetidos, con llaves {} (pero sin pares clave:valor). Apenas se usa en este proyecto; conviene conocerlo para una pregunta como "¿qué tipos distintos tiene mi equipo?":
team_types = {"Fire", "Water", "Fire", "Electric"}
# team_types == {"Fire", "Water", "Electric"} -> el "Fire" repetido se descarta
Es la herramienta natural cuando solo te importa qué cosas distintas hay, no cuántas ni en qué orden.
Sintaxis Python (resumen de referencia)¶
# LISTA
moves = ["Ember", "Scratch"]
moves[0] # "Ember" (índice desde 0)
len(moves) # 2
moves.append("Growl") # añade al final
"Ember" in moves # True
for m in moves: ... # recorrer
# DICCIONARIO
POKEDEX = {"Pikachu": {"hp": 35}}
POKEDEX["Pikachu"] # {"hp": 35}
POKEDEX["Pikachu"]["hp"] # 35
"Pikachu" in POKEDEX # True (busca en claves)
POKEDEX.get("Mewtwo") # None (no rompe si falta)
TYPE_CHART["fire"]["grass"] # 2.0 (dict de dicts)
# TUPLA
turn = ("Charmander", "Squirtle")
turn[0] # "Charmander" (no se puede reasignar)
# CONJUNTO
types = {"Fire", "Water", "Fire"} # -> {"Fire", "Water"}
Gotchas / Anti-patterns¶
| Anti-pattern | Qué pasa | Cómo hacerlo bien |
|---|---|---|
moves[1] esperando el primero |
El primero es moves[0]; los índices empiezan en 0 |
moves[0] para el primero |
moves[4] con 4 elementos |
IndexError: el último es moves[3] |
El último válido es moves[len(moves)-1] o moves[-1] |
POKEDEX["Mewtwo"] sin comprobar |
KeyError y el programa muere |
if "Mewtwo" in POKEDEX: o .get(...) |
new = moves.append("X") |
append devuelve None, no la lista |
moves.append("X") y sigue usando moves |
turn[0] = "Pikachu" (tupla) |
TypeError: las tuplas son inmutables |
Usa una lista si necesitas modificar |
| Buscar en la Pokédex recorriendo una lista | Lento y verboso | Usa un dict y accede por clave directa |
Olvidar el 1.0 por defecto en la tabla de tipos |
KeyError en combinaciones neutras |
effectiveness() devuelve 1.0 si falta la clave |
Tu turno¶
No implementas nada todavía: la tarea es leer y rastrear con el dedo cómo el código usa estas estructuras. Abre pokemon/data.py y localiza la Pokédex (un dict con los 10 Pokémon: Charmander, Squirtle, Bulbasaur, Pikachu, Geodude, Rattata, Charizard, Blastoise, Venusaur, Raichu). Para cada uno, anota: ¿la clave es el nombre? ¿el valor es otro dict? ¿qué claves internas tiene (type, hp, attack, defense)?
Luego abre pokemon/types.py y localiza TYPE_CHART. Sin ejecutar nada, predice en papel el valor de:
TYPE_CHART["water"]["fire"]TYPE_CHART["grass"]["water"]- la efectividad de
"normal"contra"rock"(¿está en la tabla? si no, ¿cuál es el valor por defecto?)
Comprueba que tu lectura de la tabla es correcta ejecutando solo las pruebas de tipos:
Estas pruebas son la especificación de cómo debe comportarse effectiveness(). Léelas: te dicen exactamente qué multiplicador se espera para cada combinación. Es normal que fallen ahora; las harás pasar en el documento 05 al implementar effectiveness().
Conexiones¶
- 02-variables-y-tipos — una estructura de datos contiene valores de los tipos del doc 02
- 04-condicionales-y-bucles —
forpara recorrer listas,indentro deif - 05-funciones —
effectiveness()encapsula el acceso aTYPE_CHARTcon default1.0 - Ficheros futuros:
pokemon/data.py(POKEDEXcomodict),pokemon/types.py(TYPE_CHARTcomodictdedicts),pokemon/pokemon.py(movescomolist)
Resumen mental¶
Cuando una sola variable no basta, usas una estructura de datos. Una lista guarda valores en orden y los indexas por posición empezando en cero: los movimientos de un Pokémon y el equipo son listas;
len()cuenta,append()añade,incomprueba pertenencia, y se recorren confor. Un diccionario mapea clave a valor y permite buscar directamente sin recorrer: la Pokédex usa el nombre como clave y los datos del Pokémon como valor, y la tabla de tipos es un diccionario de diccionarios dondeTYPE_CHART["fire"]["grass"]da el multiplicador de Fuego contra Planta. Acceder con una clave inexistente lanzaKeyError; por esoeffectiveness()devuelve1.0por defecto en combinaciones neutras. Una tupla es una lista inmutable, ideal para agrupar el par(attacker, defender)de un turno. Un conjunto guarda elementos únicos sin orden, útil para "qué tipos distintos hay". Elegir bien lista vs diccionario —recorrer vs buscar por nombre— es media batalla de diseño ganada.