10 — El bucle de batalla¶
Por qué importa¶
Hasta ahora tenemos piezas sueltas: una tabla de tipos y una función que calcula el daño de un golpe. Pero un golpe no es una batalla. Una batalla es: dos Pokémon, turnos que se alternan, alguien elige movimiento, se aplica daño, se comprueba si alguien cayó, y se repite hasta que uno se queda sin vida. Ese "se repite hasta que" es un bucle, y este documento construye exactamente esa lógica.
Es la pieza que convierte cálculos en juego. También es donde aparece la diferencia entre el jugador (que decide su movimiento) y el rival (que lo elige al azar).
Schema / Modelo mental (diagrama ASCII)¶
Un turno es una pequeña máquina de estados. La batalla entera es ese turno repetido hasta que alguien se desmaya.
+-------------------------------+
| INICIO DEL TURNO |
+---------------+----------------+
|
v
+-------------------------------+
| El jugador elige movimiento |
| (input por teclado, ui.py) |
+---------------+----------------+
|
v
+-------------------------------+
| player ataca a rival |
| dmg = calculate_damage(...) |
| rival.take_damage(dmg) |
+---------------+----------------+
|
v
rival.is_fainted() ?
SI -> GANA EL JUGADOR (fin)
NO -> sigue
|
v
+-------------------------------+
| El rival elige movimiento |
| random.choice(rival.moves) |
+---------------+----------------+
|
v
+-------------------------------+
| rival ataca a player |
| dmg = calculate_damage(...) |
| player.take_damage(dmg) |
+---------------+----------------+
|
v
player.is_fainted() ?
SI -> GANA EL RIVAL (fin)
NO -> volver a INICIO DEL TURNO
La condición que mantiene vivo el bucle es: "ninguno de los dos se ha desmayado todavía". En cuanto uno cae, salimos y declaramos ganador.
Las piezas de apoyo: Pokemon y sus métodos¶
El bucle se apoya en dos comportamientos del objeto Pokemon:
pokemon.take_damage(n)-> restanpuntos de vida al Pokémon. Es la única forma de aplicar daño; el bucle calcula concalculate_damagey entrega el número atake_damage.pokemon.is_fainted()-> devuelveTruesi su vida llegó a 0 (o menos). Es la pregunta de "¿se acabó para este Pokémon?".
El bucle nunca toca la vida directamente; siempre pasa por estos métodos. Eso mantiene la regla "el Pokémon gestiona su propia vida" en un solo sitio.
Jugador vs rival: la única diferencia real¶
El esqueleto del turno es idéntico para ambos: elegir movimiento, calcular daño, aplicarlo, comprobar desmayo. Lo único que cambia es cómo se elige el movimiento:
- Jugador: le preguntamos. La función de
ui.pymuestra los movimientos disponibles y lee por teclado cuál quiere. Devuelve un objetoMove. - Rival: no le preguntamos a nadie.
random.choice(rival.moves)saca uno de su lista al azar. Es un rival "tonto" pero suficiente para el juego.
Mantén esa frontera limpia: la entrada por teclado vive en ui.py, no dentro del bucle. El bucle solo dice "necesito un movimiento del jugador" y delega.
El orden del turno y por qué importa el chequeo intermedio¶
El jugador ataca primero. Antes de dejar atacar al rival, comprobamos si el rival se desmayó. Si no hiciéramos ese chequeo intermedio, un rival ya derrotado todavía podría devolver el golpe — el jugador ganaría pero recibiría daño de un Pokémon "muerto". Por eso hay dos comprobaciones de desmayo por turno, una después de cada ataque, no una sola al final.
Sintaxis Python (al final)¶
import random
class Battle:
def __init__(self, player, rival):
self.player = player
self.rival = rival
def run(self):
while not self.player.is_fainted() and not self.rival.is_fainted():
# --- turno del jugador ---
move = choose_player_move(self.player) # de ui.py
dmg = calculate_damage(self.player, self.rival, move)
self.rival.take_damage(dmg)
if self.rival.is_fainted():
break
# --- turno del rival ---
rival_move = random.choice(self.rival.moves)
dmg = calculate_damage(self.rival, self.player, rival_move)
self.player.take_damage(dmg)
if self.player.is_fainted():
break
# --- declarar ganador ---
if self.rival.is_fainted():
return self.player
return self.rival
Conceptos de sintaxis:
- while not A and not B: -> "sigue mientras NINGUNO de los dos se haya desmayado". Equivale a "para en cuanto uno caiga".
- break -> sale del while inmediatamente. Lo usamos en el chequeo intermedio para no dejar contraatacar a un Pokémon ya vencido.
- random.choice(lista) -> devuelve un elemento al azar de una lista (la usamos sobre rival.moves).
- El método run devuelve el Pokémon ganador en vez de imprimirlo; quién lo muestre es decisión de quien llama (lo verás en el último documento).
La condición del while y los break parecen redundantes, pero no lo son: el while evita entrar a un turno si la batalla ya acabó; los break cortan a mitad de turno cuando el primero de los dos ataques ya decidió el combate.
Gotchas / Anti-patterns (tabla)¶
| Anti-pattern | Síntoma | Fix |
|---|---|---|
| Un solo chequeo de desmayo al final del turno | Un Pokémon ya derrotado devuelve el golpe | Comprobar is_fainted() después de CADA ataque, con break |
while True: sin condición de salida clara |
Bucle infinito si olvidas el break |
while not p.is_fainted() and not r.is_fainted() como red de seguridad |
Restar vida con pokemon.hp -= dmg directo |
La regla de "vida no baja de 0 / desmayo" se duplica o se olvida | Siempre pokemon.take_damage(dmg) |
Pedir input por teclado dentro de Battle |
Imposible testear; mezcla lógica con UI | La entrada vive en ui.py; el bucle solo la invoca |
random.choice(rival) en vez de rival.moves |
Error: eliges sobre el Pokémon, no sus movimientos | random.choice(rival.moves) |
| Aplicar daño al atacante en vez de al defensor | El que pega se hace daño a sí mismo | defender.take_damage(...), atacante y defensor bien ordenados |
Imprimir el ganador dentro de run() |
Lógica acoplada a la consola, no reutilizable | run() devuelve el ganador; el caller lo muestra |
Tu turno¶
Implementa la lógica de Battle en pokemon/battle.py: el constructor con los dos Pokémon y el método que corre el bucle hasta KO, alterna turnos (jugador primero, vía la función de ui.py; rival con random.choice), aplica daño con take_damage(), comprueba is_fainted() tras cada ataque y devuelve el Pokémon ganador.
Comando de test exacto:
(Los tests fijan la semilla del azar y simulan la entrada del jugador, así que el resultado del bucle es determinista y comprobable.)
Conexiones¶
- 08-la-tabla-de-tipos — vía
calculate_damage, cada ataque consulta la tabla. - 09-la-formula-de-dano — el bucle llama a
calculate_damage()y entrega el resultado atake_damage(). - 11-juntandolo-todo —
main.pycrea unBattle, lo corre y muestra el ganador querun()devuelve. - Ruta futura:
pokemon/battle.py(claseBattle),pokemon/ui.py(elección del jugador),pokemon/pokemon.py(take_damage,is_fainted). - Test:
tests/test_battle.py.
Resumen mental¶
El bucle de batalla convierte cálculos sueltos en juego. Un turno es una máquina de estados: el jugador elige movimiento (input vía
ui.py), se calcula daño concalculate_damage, se aplica conrival.take_damage(dmg), se compruebarival.is_fainted(); si no cayó, el rival elige al azar conrandom.choice(rival.moves), ataca, y se compruebaplayer.is_fainted(). La batalla es ese turno dentro de unwhile not player.is_fainted() and not rival.is_fainted(). Clave: hay DOS chequeos de desmayo por turno (uno tras cada ataque, conbreak), no uno al final — si no, un Pokémon ya vencido contraataca. La única diferencia jugador/rival es cómo se elige el movimiento: preguntar vsrandom.choice. Reglas de higiene: nunca tocarhpdirecto (siempretake_damage), la entrada por teclado vive enui.pyno enBattle(testeabilidad), yrun()devuelve el ganador en vez de imprimirlo (desacoplar lógica de consola). Elwhiley losbreakse complementan: elwhileevita entrar a turno con la batalla ya acabada, losbreakcortan a mitad de turno.