Construí tu propio videojuego con Programación Orientada a Objetos. Este apunte tiene todo el trabajo práctico en papel: la consigna, la teoría que vas a necesitar, el código paso a paso, los desafíos, la entrega y la rúbrica. Es la versión imprimible de la guía interactiva, y se puede hacer entero desde acá.
__init__, self, return y el if que la protege.super().for sin un solo if.while que hace chocar todo lo que construiste.Bienvenido a la Arena de Combate. Tu misión es construir un videojuego usando Programación Orientada a Objetos.
Nadie te va a dar el juego terminado. Lo vas a ir construyendo vos, etapa por etapa, y en cada una el juego va a poder hacer una cosa más que antes. Al principio tu personaje solo va a existir. Después va a poder recibir daño. Después va a tener un arma. Después va a aparecer un enemigo. Y en algún momento vas a tener una pelea por turnos funcionando en tu computadora.
La parte importante no es el juego. Es que al final vas a poder explicar por qué lo hiciste así, y eso te va a servir para cualquier problema, no solo para este.
Que al terminar puedas explicar, con tus palabras y señalando tu propio código, qué es una clase,
qué es un objeto, qué son los atributos y los métodos, cómo funciona la herencia, para qué sirve
super(), qué diferencia hay entre print() y return, qué es la
composición, qué es el polimorfismo y cómo varios objetos colaboran entre ellos.
Individual o en parejas de máximo dos personas. Si son dos, los dos tienen que poder explicar todo el código: no sirve repartirse "vos las clases, yo el menú".
Sin librerías raras y sin cosas que no vimos. Todo lo que necesitás ya lo sabés: variables,
if, while, funciones, listas y clases.
Abrí un archivo nuevo, ponele juego.py y tenelo al lado mientras avanzás. Cada etapa
le agrega algo. No arranques de cero en cada etapa.
Varias etapas empiezan sin computadora. No es para hacerte perder tiempo: es para que el código salga solo cuando llegues al teclado.
Este es el camino que se repite en todas las etapas, y el que vas a usar el día que te den un problema que no tenga nada que ver con videojuegos.
En el sexto lugar. Cinco pasos antes de tocar el teclado. No memorices: razoná. Si arrancás por el código, te vas a trabar — y eso ya lo vimos en las dos clases.
super().while con todo adentro.Este apunte es el mismo trabajo práctico que la guía web, que además corrige las actividades sola y
tiene simuladores para probar el combate sin instalar nada:
pedroibanez97.github.io/P.O.O/tp-arena-de-combate.html
Podés usar los dos: la guía para trabajar y este apunte para estudiar, subrayar y tener a mano.
Nada de lo que viene es nuevo: es todo de las clases 1 y 2. Está acá para que no tengas que ir a buscarlo mientras programás.
No hace falta que te lo aprendas de memoria antes de empezar. Arrancá el trabajo práctico y volvé a esta sección cada vez que te trabes. Esa es la forma en que esto se aprende de verdad.
Lo que el objeto guarda adentro. Son sustantivos. En Python los llamamos atributos.
Las acciones que sabe realizar. Son verbos y llevan (). En Python los
llamamos métodos.
Decilo en voz alta con las dos frases: "el personaje tiene…" y "el personaje puede…".
Una de las dos va a sonar mal. "El personaje tiene atacar" suena horrible: entonces
atacar() es un método.
La clase es el molde: describe qué van a tener y qué van a poder hacer todos, pero no existe. El objeto es uno concreto, con valores propios. Una clase, todos los objetos que quieras.
class Arma: # el molde ... espada = Arma("Espada", 25) # un objeto arco = Arma("Arco", 15) # otro
La clase describe. El objeto existe. Cambiarle el daño a la espada no toca al arco: son independientes.
Los atributos son los datos que el objeto guarda; los métodos, las funciones que viven adentro del objeto y trabajan sobre esos datos.
espada.nombre # atributo: un dato espada.usar() # método: una acción
Los paréntesis son la pista: sin paréntesis lo leés o lo cambiás; con paréntesis se ejecuta.
__init__
Es un método especial que se ejecuta solo, una sola vez: justo cuando escribís
Arma("Espada", 25). Ahí el objeto recibe sus datos y queda armado. También se lo llama
constructor.
class Arma: def __init__(self, nombre, danio): self.nombre = nombre self.danio = danio
Los valores que van entre paréntesis al crear el objeto caen en los parámetros de
__init__.
self
Quiere decir "yo". self.vida es "mi vida": el objeto refiriéndose a sus
propios datos, para no confundirlos con los de otro objeto.
def recibir_danio(self, cantidad): self.vida = self.vida - cantidad
El error número uno: olvidarte el self. al guardar un atributo. Si escribís
vida = vida, el dato no se guarda en el objeto y después "desaparece".
Escribimos una sola vez lo que comparten y cada hijo agrega solo lo suyo. Es una sola cosa que recibe todo lo del padre.
# Jugador ES UN Personaje class Jugador(Personaje): ...
El test: "un jugador es un personaje". Si la frase suena rara, no era herencia. Y al revés: si estás copiando y pegando una clase, ahí hay una herencia esperando.
super()
super() quiere decir "la clase de arriba". Traducido al castellano:
"Padre, encargate vos de nombre y vida. Yo me ocupo de lo mío."
def __init__(self, nombre, vida, arma): super().__init__(nombre, vida) self.arma = arma
Va como primera línea del __init__ del hijo. Así cada dato se escribe en
un solo lugar del programa.
Cada escalón suma, no reemplaza. El guerrero no recibe solo lo del jugador: recibe todo lo que hay más arriba en la cadena, y todo vive en un solo objeto.
Personaje → Jugador → Guerrero nombre arma fuerza vida
Dos o tres niveles alcanzan casi siempre. Si necesitás ocho, casi seguro no era herencia lo que buscabas.
print() vs return
print() manda algo a la pantalla: lo ve una persona y ahí termina.
return te entrega el valor: lo podés guardar, comparar en un if o
volver a usar.
def esta_vivo(self): return self.vida > 0 if jugador.esta_vivo(): print("Seguí jugando")
La pregunta que resuelve la duda: ¿lo necesita una persona o lo necesita el programa?
Un atributo también puede ser otro objeto. Son dos objetos distintos, conectados: el jugador guarda adentro un arma entera, no un texto ni un número.
self.arma = arma # la línea de siempre print(jugador.arma.nombre) # "el nombre del arma del jugador"
El punto se lee "de", de derecha a izquierda. Y el jugador no sabe cuánto daño hace: le pregunta al arma. Cada objeto se ocupa de lo suyo.
Le damos la misma orden a objetos distintos y cada uno responde a su manera. El código que los usa no cambia nunca, aunque agregues diez enemigos más.
for enemigo in [dragon, goblin, lobo]: print(enemigo.atacar())
¿Cuántos if hay en ese for? Cero. Cada objeto sabe cómo responder.
jugador.vida = -999999
Si cualquier parte del programa puede escribir eso, el juego se rompe: personajes con vida negativa, monedas infinitas, trampa pura. El dato miente.
def recibir_danio(self, cantidad): self.vida = self.vida - cantidad if self.vida < 0: self.vida = 0
Ese if es la única razón por la que la vida no queda en negativo. Y ese control
vive adentro del objeto: solo el objeto decide cómo cambian sus propios datos.
class Nombre: # definir una clase (con mayúscula) def __init__(self, dato1, dato2): # el constructor self.dato1 = dato1 # guardar un atributo self.dato2 = dato2 def hacer_algo(self): # un método que NO devuelve self.dato1 = self.dato1 + 1 def contestar(self): # un método que SÍ devuelve return self.dato1 class Hija(Nombre): # heredar def __init__(self, dato1, dato2, extra): super().__init__(dato1, dato2) # el padre hace su parte self.extra = extra # el hijo agrega lo suyo objeto = Hija("a", "b", "c") # crear el objeto print(objeto.dato1) # leer un atributo objeto.hacer_algo() # ejecutar un método resultado = objeto.contestar() # guardar lo que devuelve
if, while y listas, que ya sabías desde antes de POO.Te pasan la captura de un juego y te dicen: "programalo". En la pantalla hay un héroe llamado Aria (nivel 3, 80 de 100 de vida, 40 puntos de experiencia), un goblin con 33 de 60 de vida, una espada, una poción y el fondo de la arena.
¿Qué objetos necesitamos para construir un juego de combate?
Marcá todo lo que te parece que merece ser un objeto del programa, o sea, algo con su propia clase. Ojo: no todo lo que ves en la pantalla es un objeto.
Después de marcar, escribí al lado de cada una por qué. Esa explicación vale más que la marca. La solución está en la sección 16.
Un objeto es algo que tiene información y además hace algo. Si solo guarda un dato y no hace nada, casi siempre es un atributo de alguien más. Que algo se te ocurra no significa que merezca ser una clase.
Le hacemos las dos preguntas al héroe. Escribí A (atributo) o M (método) al lado de cada uno, y probá leyéndolo en voz alta con "el héroe tiene…" / "el héroe puede…".
Mirá al héroe y al goblin. Los dos tienen nombre, los dos tienen vida, a los dos les pueden pegar y los dos pueden morir. Si todo eso lo comparten, escribirlo dos veces sería un error: acordate de que si estás copiando y pegando una clase, ahí hay una herencia esperando.
Entonces creamos una clase de arriba con lo que comparten. La vamos a llamar Personaje, y de ahí van a heredar el jugador y el enemigo.
¿Qué información tiene cada clase y qué puede hacer cada una?
Primero la tuya. No mires ninguna solución todavía: escribí lo que vos creés, aunque no estés seguro. Esto se hace en papel y tarda cinco minutos; saltearlo tarda dos horas.
| Clase | Atributos · ¿qué tiene? | Métodos · ¿qué puede hacer? |
|---|---|---|
| Personaje la de arriba | ||
| Jugador | ||
| Enemigo | ||
| Arma |
Ojo: en Jugador y en Enemigo no repitas nombre ni vida. Eso ya
está en Personaje y lo van a heredar. En la tabla de un hijo va solo lo que agrega.
No es qué atributos existen: es en qué clase va cada uno. La regla es siempre la misma:
arriba va solo lo que comparten todos. Si le ponés experiencia a Personaje, entonces
los goblins, las piedras y los cofres también tienen experiencia. Y el arma no va en
Personaje: no todos los personajes tienen un arma.
Dos flechas, dos relaciones distintas. La de arriba dice ES UN y es herencia. La del costado dice TIENE UN y es composición. Copiá este dibujo en tu carpeta: es parte de la entrega final.
Si vos pusiste vida_maxima en Personaje, o un método mostrar_info(), o le
pusiste fuerza al Jugador: está bien, siempre que puedas justificarlo. Diseñar es
tomar decisiones y casi nunca hay una sola correcta.
Personaje¿Por qué conviene empezar por Personaje y no por Jugador?
Porque los demás van a heredar de ella: primero tiene que existir el padre. Y además, poner lo común arriba es justamente lo que evita repetir código.
class Personaje: def __init__(self, nombre, vida): self.nombre = nombre self.vida = vida
def recibir_danio(self, cantidad): self.vida = self.vida - cantidad
En la clase 2 lo escribimos recibir_daño, con ñ. Acá lo escribimos
recibir_danio, sin ñ. Python acepta las dos, pero sin ñ te evitás problemas al copiar
código, al cambiar de teclado o al compartir el archivo. Es exactamente la misma idea, con otro nombre.
def esta_vivo(self): return self.vida > 0
True o False.esta_vivo() usa return?
Porque contesta una pregunta y el programa va a usar esa respuesta adentro de un
if y del while de la pelea. Si hiciera print, el
while sería imposible de escribir.
recibir_danio() no?
Porque hace un cambio en el objeto y listo: modifica la vida y no tiene nada que entregar. Siempre la misma pregunta: ¿lo necesita una persona o el programa?
esta_vivo()?La vida queda en −400. Y un personaje con vida negativa no existe: el dato miente. Esto ya lo arreglaste una vez, en el ejercicio de la mascota de la clase 1.
Hacé que la vida nunca quede por debajo de cero. Completá los tres huecos:
def recibir_danio(self, cantidad): self.vida = self.vida - cantidad ______ self.vida ___ 0: self.vida = ___
Pista: primero restás y después te fijás si te pasaste. El arreglo tiene que vivir adentro del objeto, porque es el objeto el que decide cómo cambian sus propios datos. Eso es encapsulación.
Escribí la clase completa en juego.py y, antes de ejecutar, anotá en la carpeta
qué esperás que imprima cada línea:
aria = Personaje("Aria", 100) aria.recibir_danio(30) print(aria.vida) aria.recibir_danio(500) print(aria.vida) print(aria.esta_vivo())
Arma¿Un Arma es un Personaje? ¿Debería heredar de Personaje?
No. Leelo en voz alta: "una espada es un personaje". Suena raro, y cuando la frase suena rara, la respuesta es no. ¿Cuánta vida tiene una espada? ¿Se muere? Es otra cosa: una clase aparte, que no hereda de nadie.
class Arma: def __init__(self, nombre, danio): self.nombre = nombre self.danio = danio def usar(self): return self.danio
usar() devuelve
Porque ese daño no es para mirarlo: es para restárselo a la vida del enemigo. Lo necesita el
programa, no una persona. Con print el número se vería en pantalla y se perdería,
y nadie podría agarrarlo.
En la clase 2 este método se llamaba atacar(). Acá le decimos usar() porque
el arma no ataca sola: la usa alguien. El nombre lo elegís vos; lo que no se negocia es que
devuelva el daño.
espada = Arma("Espada", 25) arco = Arma("Arco", 15) print(espada.usar()) # ? print(arco.usar()) # ? print(espada.nombre) # ? resultado = espada.usar() # el número queda GUARDADO print(resultado - 5) # ← puedo seguir haciendo cuentas
usar() devuelve, el valor se puede guardar en
una variable y seguir usándolo. Con print, resultado valdría None.Para cada método del juego, escribí cuál corresponde y por qué:
Inventá el arma con la que va a pelear tu personaje: ponele el nombre que quieras y decidí cuánto daño hace. Te va a acompañar durante todo el trabajo práctico.
Escribila en juego.py y probá print(mi_arma.usar()).
Tenemos un Personaje y tenemos un Arma. Están sueltos: ninguno de los dos sabe que el otro existe. Hay que decidir cómo se relacionan — y eso se decide antes de escribir código, leyendo dos frases en voz alta.
"Un jugador es un arma." Suena horrible. Y cuando la frase suena rara, la respuesta es no. Acá no va herencia.
"Un jugador tiene un arma." Esa sí suena natural. Entonces va composición: dos objetos distintos, conectados.
super()class Jugador(Personaje): def __init__(self, nombre, vida): super().__init__(nombre, vida)
(Personaje) es todo lo que hace falta para decir "Jugador ES UN
Personaje": con eso el jugador ya tiene recibir_danio() y esta_vivo() sin
escribirlos. Y super() es pedirle al padre que guarde nombre y
vida, para no repetirlos.__init__ de
Jugador con los datos que le pasamos.super().__init__(nombre, vida)
sube a Personaje, que guarda nombre y vida.class Jugador(Personaje): def __init__(self, nombre, vida): self.vida = vida # se olvidó del super() aria = Jugador("Aria", 100) print(aria.nombre)
¿Qué error da y por qué? Escribilo con tus palabras antes de mirar la sección 16.
class Jugador(Personaje): def __init__(self, nombre, vida, arma): super().__init__(nombre, vida) self.arma = arma # ← ¡acá! espada = Arma("Espada", 25) aria = Jugador("Aria", 100, espada)
espada entra adentro del objeto aria.self.arma = arma es la misma línea de siempre: self.algo = algo. Lo
único distinto es qué estamos guardando: no es un texto ni un número, es un objeto entero.
Un atributo también puede ser otro objeto.
print(aria.nombre) # Aria print(aria.arma.nombre) # Espada print(aria.arma.usar()) # 25
aria.arma.nombre es
"el nombre del arma de Aria". Cada punto es un paso más adentro. Si te perdés en
una línea larga con muchos puntos, leela así y se ordena sola.def atacar(self): return self.arma.usar()
En return self.arma.usar() participan dos objetos: el jugador le pide
el daño a su arma y devuelve lo que el arma le contestó. No hay ninguna cuenta en el código del
jugador. Cada objeto se ocupa de lo suyo: eso es que los objetos "hablen entre ellos".
hacha = Arma("Hacha", 40) aria.arma = hacha print(aria.atacar()) # 40
¿Qué línea de la clase Jugador tuvimos que cambiar? Ninguna. El jugador siempre
supo lo mismo: "tengo un arma y le pido que ataque". Qué arma sea, no es su problema. Con 50 armas
distintas la clase sigue igual de corta; si hubiéramos usado herencia, cada arma nueva sería un tipo de
jugador nuevo. Un desastre.
Para cada par, escribí ES UN (herencia) o TIENE UN (composición). Leelo en voz alta con las dos frases antes de contestar.
Jugador completa. Creá tu arma y tu jugador, y mostrá
print(jugador.atacar()).Jugador.Tu jugador pega, pero del otro lado no hay nadie. Falta el enemigo. Nuestro goblin
pelea a mordiscones: no usa arma, tiene un danio propio. (También se podría darle un
Arma llamada "mordisco" — las dos decisiones son defendibles; lo importante es poder
justificarla.)
class Enemigo(Personaje): def __init__(self, nombre, vida, danio): super().__init__(nombre, vida) self.danio = danio def atacar(self): return self.danio
Jugador, cambiando lo que agrega. Si ya lo podés
escribir casi sin pensar, es que entendiste el patrón.Los atributos nombre y vida, y los métodos recibir_danio() y
esta_vivo(). Ninguno está escrito adentro de Enemigo: los hereda.
El atributo danio y el método atacar(). Eso es lo único suyo. Lo común
(nombre y vida) está arriba; lo específico, abajo.
goblin = Enemigo("Goblin", 60, 12) esqueleto = Enemigo("Esqueleto", 45, 18) lobo = Enemigo("Lobo", 70, 10)
Marcá cuáles de estas líneas funcionan y cuáles dan error, y explicá por qué:
Y después, en la carpeta: ¿qué atributos de tu enemigo vienen de Personaje y cuáles
son propios de Enemigo?
Tu juego crece: querés que existan clases de personaje, como en cualquier RPG. Empezamos por el Guerrero, que es un jugador con fuerza. Hacé el test de siempre, escalón por escalón: "un guerrero es un jugador" ✓, "un jugador es un personaje" ✓. Los dos escalones aguantan la frase.
class Guerrero(Jugador): def __init__(self, nombre, vida, arma, fuerza): super().__init__(nombre, vida, arma) self.fuerza = fuerza
Guerrero nunca menciona vida. Eso es asunto de
Personaje.Cada escalón suma, no reemplaza. Guerrero le pide a Jugador, y
Jugador le pide a Personaje. Cada clase se ocupa solamente de lo suyo y nadie
repite el trabajo de otro. Los cuatro atributos terminan viviendo en un solo objeto.
espada = Arma("Espada", 25) aria = Guerrero("Aria", 100, espada, 8) print(aria.nombre) print(aria.arma.nombre) print(aria.fuerza) print(aria.esta_vivo()) print(aria.danio)
Escribí qué imprime cada línea. Una de las cinco da error: encontrala y explicá por qué.
Una cadena de ocho clases hace que, para entender una, haya que leer ocho. Dos o tres niveles alcanzan casi siempre. Si necesitás ocho, casi seguro no era herencia lo que buscabas.
Guerrero y creá uno. Imprimí sus cuatro datos.Jugador: un Mago que agregue
magia.Guerrero pegue más fuerte que un jugador común
usando su fuerza. Escribile su propio atacar(). Eso ya es polimorfismo,
que es justo lo que viene ahora.Tu arena ya tiene goblins, esqueletos y lobos… y los tres pegan exactamente igual. Aburridísimo. Queremos que el dragón sea temible y que el goblin sea un estorbo menor.
if enemigo.tipo == "dragon": danio = enemigo.danio + 15 elif enemigo.tipo == "goblin": danio = enemigo.danio elif enemigo.tipo == "esqueleto": danio = enemigo.danio * 2 # ... y así con cada enemigo nuevo
class Dragon(Enemigo): def atacar(self): return self.danio + 15 class Goblin(Enemigo): def atacar(self): return self.danio
El problema del primero no es que no funcione: es que crece con cada enemigo nuevo.
Cada vez hay que volver a ese if y agregarle un elif — y si el mismo
if está en tres lugares del programa, hay que acordarse de los tres.
Ya sabíamos que un hijo puede agregar cosas. Acá usamos la otra mitad de la herencia:
un hijo también puede reemplazar un método del padre. Ninguno de los dos escribe
__init__: lo heredan tal cual, porque no agregan ningún atributo nuevo. Solo cambian
cómo responden a una orden.
dragon = Dragon("Dragón", 90, 20) goblin = Goblin("Goblin", 60, 12) lobo = Goblin("Lobo", 70, 10) for enemigo in [dragon, goblin, lobo]: print(enemigo.nombre, "ataca con", enemigo.atacar())
if hay en ese for?Cero. El for no sabe ni le importa quién es cada uno: cada objeto sabe cómo
responder. Y si mañana agregás un Fantasma, no tocás una sola línea del
for. El juego solo grita atacar(); cada personaje sabe qué significa eso
para él.
¿Hacía falta escribir atacar() en Goblin? No. Si lo borrás, el goblin
igual sabe atacar: lo hereda de Enemigo, que hace exactamente lo mismo. Lo dejamos escrito
solo para poder comparar las dos clases una al lado de la otra. En un programa de verdad, un hijo
escribe un método solo cuando lo hace distinto.
Esqueleto(Enemigo) cuyo atacar() devuelva
self.danio * 2.Slime(Enemigo) que pegue la mitad: self.danio // 2.for.La reflexión: agregaste dos enemigos nuevos. ¿Tuviste que cambiar una sola línea del
for? ¿Qué pasaría si lo hubieras hecho con if y elif, y mañana
quisieras veinte enemigos?
Todas las piezas existen y ninguna se toca. Tenés un jugador con arma, un enemigo que pega y dos personajes que saben decir si siguen vivos. Falta el pedazo de programa que los haga pelear.
Numerá del 1 al 7 en qué orden tienen que pasar las cosas en cada vuelta del combate:
whilewhile aria.esta_vivo() and goblin.esta_vivo():
and y no or
porque basta con que uno se muera para que la condición sea falsa y el combate termine; con
or, el sobreviviente le seguiría pegando a un muerto.Es puro return. Los objetos contestan True o False y el
programa decide con eso. Si esta_vivo() hiciera print, esta pelea sería
imposible de escribir. Ahí se cobra todo lo de la clase 2.
danio = aria.atacar() goblin.recibir_danio(danio) print(aria.nombre, "hace", danio, "de daño ·", goblin.nombre, "queda con", goblin.vida)
atacar() usa return, ese número le queda en la mano, guardado en
danio. Si el arma hiciera print, acá no habría nada que guardar.print: esto lo lee
una persona, no lo necesita el programa.Si el goblin quedó con 0 de vida y el enemigo igual contraataca, un enemigo muerto le sigue
pegando al jugador. El programa no da error: por eso es peligroso. Anda igual, pero el juego
queda mal. El while corta recién en la vuelta siguiente.
if goblin.esta_vivo(): golpe = goblin.atacar() aria.recibir_danio(golpe) print(goblin.nombre, "hace", golpe, "de daño ·", aria.nombre, "queda con", aria.vida)
True o False es el que nos
salva.espada = Arma("Espada", 25) aria = Jugador("Aria", 100, espada) goblin = Goblin("Goblin", 60, 12) print("¡Empieza el combate!") while aria.esta_vivo() and goblin.esta_vivo(): danio = aria.atacar() goblin.recibir_danio(danio) print(aria.nombre, "hace", danio, "de daño ·", goblin.nombre, "queda con", goblin.vida) if goblin.esta_vivo(): golpe = goblin.atacar() aria.recibir_danio(golpe) print(goblin.nombre, "hace", golpe, "de daño ·", aria.nombre, "queda con", aria.vida) if aria.esta_vivo(): print("Ganó", aria.nombre) else: print("Ganó", goblin.nombre)
class Jugador(Personaje), class Goblin(Enemigo) y
super().__init__(nombre, vida). Nadie volvió a escribir vida.
self.arma = arma, y después self.arma.usar(): el jugador le pregunta a su
arma.
esta_vivo() hace funcionar al while; atacar() entrega el
número que se resta.
Cambiá Goblin por Dragon en una sola línea y la pelea entera se comporta
distinto. Sin tocar el while.
El if adentro de recibir_danio(): nadie toca la vida desde afuera y nunca
queda en negativo.
aria.atacar() es una línea. Adentro pasan varias cosas, pero eso no es problema de
quien usa el objeto.
Dragon en una sola línea. ¿Cambiaste algo del
while?def atacar_a(self, otro): danio = self.arma.usar() otro.recibir_danio(danio) return danioEscribilo en tu
Jugador y reemplazá las dos líneas del turno por
aria.atacar_a(goblin). Después contestá: si ya le restó la vida al otro, ¿para qué
encima devuelve el daño?Ganaste la pelea… y no pasó absolutamente nada. Tu personaje sigue igual de fuerte que al empezar. Falta la recompensa.
Fijate que cuando un programa crece siempre es lo mismo: o le agregamos datos a un
objeto, o le agregamos acciones. Acá agregamos las dos cosas. Y van en
Jugador, no en Personaje: solo el jugador sube de nivel.
class Personaje: def __init__(self, nombre, vida): self.nombre = nombre self.vida = vida self.vida_maxima = vida # ← nuevo: hasta dónde se puede curar
class Jugador(Personaje): def __init__(self, nombre, vida, arma): super().__init__(nombre, vida) self.arma = arma self.nivel = 1 # ← nuevo self.experiencia = 0 # ← nuevo def ganar_experiencia(self, cantidad): self.experiencia = self.experiencia + cantidad print(self.nombre, "ganó", cantidad, "de experiencia") if self.experiencia >= 100: self.subir_nivel() def subir_nivel(self): self.nivel = self.nivel + 1 self.experiencia = self.experiencia - 100 self.vida_maxima = self.vida_maxima + 20 self.vida = self.vida_maxima print(self.nombre, "subió al nivel", self.nivel)
nivel y experiencia no se piden por parámetro: todos los
jugadores arrancan en nivel 1 con 0. Un atributo puede tener un valor fijo de arranque, sin que nadie se
lo pase — la misma idea del bonus de la clase 2: si todas las motos tienen 2 ruedas, no hace falta
pedir el dato.Si ganó 130 de experiencia, sube de nivel y le sobran 30. Con = 0 esos 30 se
pierden; restando, quedan para el próximo nivel. Es una decisión de diseño: la otra también es válida,
si la podés justificar.
subir_nivel()?
El propio jugador, desde adentro de ganar_experiencia(). Vos no tenés que acordarte de
nada: el objeto se encarga solo. Eso es encapsulación otra vez.
aria.ganar_experiencia(60) aria.ganar_experiencia(60) print(aria.nivel) print(aria.experiencia)
¿Qué imprimen las dos últimas líneas? Y una pregunta más: ¿por qué usamos
>= 100 y no == 100?
aria.ganar_experiencia(50).self.arma.danio = self.arma.danio + 5) o darle a
Arma un método mejorar() y pedírselo. ¿Cuál te parece mejor y por qué?
Esa pregunta vale más que el código.El menú no es el objetivo del trabajo práctico. Es solamente la forma de usar el sistema
que construiste. Lo importante ya está hecho: las clases, la herencia, la composición, el
return y el polimorfismo. El menú son if y elif, y eso ya lo
sabías hacer desde antes de POO.
Igual, sin menú el juego no se juega: se ejecuta y se termina solo. Y antes falta un objeto, porque una de las opciones va a ser curarse. ¿Un jugador ES una poción? No. ¿TIENE una? Sí: es exactamente el mismo caso que el arma.
class Pocion: def __init__(self, nombre, cura): self.nombre = nombre self.cura = cura def usar(self): return self.cura
Arma y Pocion son casi idénticas: guardan un número y lo
devuelven. Cuando entendés un patrón, el siguiente objeto sale solo.def curarse(self, pocion): self.vida = self.vida + pocion.usar() if self.vida > self.vida_maxima: self.vida = self.vida_maxima
if es el mismo control que en recibir_danio(), pero del otro lado: uno impide
bajar de 0, el otro impide pasar del máximo.while True: print("=== ARENA DE COMBATE ===") print("1. Ver información") print("2. Atacar") print("3. Usar poción") print("4. Ver experiencia") print("5. Salir") opcion = input("Elegí una opción: ") if opcion == "1": heroe.mostrar_info() elif opcion == "5": print("¡Hasta la próxima!") break
input() siempre devuelve texto, por eso
comparamos con "1" entre comillas y no con 1. Y while True repite
para siempre: la única forma de salir es el break. El programa completo está en la sección
siguiente.Todo lo que construiste en once etapas, junto.
Leelo entero antes de copiarlo: tenés que poder explicar cada línea. Si hay alguna que no entendés, volvé a la sección donde apareció. Y no lo uses como punto de partida: el trabajo es llegar hasta acá, no arrancar desde acá.
# ===== ARENA DE COMBATE ===== # Trabajo práctico integrador de POO class Personaje: def __init__(self, nombre, vida): self.nombre = nombre self.vida = vida self.vida_maxima = vida def recibir_danio(self, cantidad): self.vida = self.vida - cantidad if self.vida < 0: self.vida = 0 def esta_vivo(self): return self.vida > 0 def mostrar_info(self): print(self.nombre, "- Vida:", self.vida, "/", self.vida_maxima) class Arma: def __init__(self, nombre, danio): self.nombre = nombre self.danio = danio def usar(self): return self.danio class Pocion: def __init__(self, nombre, cura): self.nombre = nombre self.cura = cura def usar(self): return self.cura class Jugador(Personaje): def __init__(self, nombre, vida, arma): super().__init__(nombre, vida) self.arma = arma self.nivel = 1 self.experiencia = 0 def atacar(self): return self.arma.usar() def curarse(self, pocion): self.vida = self.vida + pocion.usar() if self.vida > self.vida_maxima: self.vida = self.vida_maxima def ganar_experiencia(self, cantidad): self.experiencia = self.experiencia + cantidad print(self.nombre, "ganó", cantidad, "de experiencia") if self.experiencia >= 100: self.subir_nivel() def subir_nivel(self): self.nivel = self.nivel + 1 self.experiencia = self.experiencia - 100 self.vida_maxima = self.vida_maxima + 20 self.vida = self.vida_maxima print(self.nombre, "subió al nivel", self.nivel) class Enemigo(Personaje): def __init__(self, nombre, vida, danio): super().__init__(nombre, vida) self.danio = danio def atacar(self): return self.danio class Goblin(Enemigo): def atacar(self): return self.danio # igual que el padre: podría no escribirse class Dragon(Enemigo): def atacar(self): return self.danio + 15 # el dragón pega más fuerte # ===== creamos los objetos ===== espada = Arma("Espada", 25) pocion = Pocion("Poción chica", 30) heroe = Jugador("Aria", 150, espada) # 150 de vida: el dragón pega fuerte enemigo = Dragon("Dragón rojo", 90, 20) # 20 + 15 = 35 de daño por golpe # ===== el menú ===== while True: print() print("=== ARENA DE COMBATE ===") print("1. Ver información") print("2. Atacar") print("3. Usar poción") print("4. Ver experiencia") print("5. Salir") opcion = input("Elegí una opción: ") if opcion == "1": heroe.mostrar_info() enemigo.mostrar_info() elif opcion == "2": danio = heroe.atacar() enemigo.recibir_danio(danio) print(heroe.nombre, "atacó con", heroe.arma.nombre, "e hizo", danio, "de daño") if enemigo.esta_vivo(): golpe = enemigo.atacar() heroe.recibir_danio(golpe) print(enemigo.nombre, "contraatacó e hizo", golpe, "de daño") else: print(enemigo.nombre, "fue derrotado") heroe.ganar_experiencia(50) elif opcion == "3": heroe.curarse(pocion) print(heroe.nombre, "usó", pocion.nombre, "- Vida:", heroe.vida) elif opcion == "4": print("Nivel:", heroe.nivel, "- Experiencia:", heroe.experiencia, "/ 100") elif opcion == "5": print("¡Hasta la próxima!") break else: print("Esa opción no existe") if not heroe.esta_vivo(): print("GAME OVER") break
Dragon por Goblin en la línea del enemigo y contá
cuántas líneas más del programa tuviste que tocar.Antes de seguir, marcá en tu archivo con un comentario dónde está cada cosa: herencia,
super(), composición, polimorfismo, return y
encapsulación. Si alguna no la encontrás, esa es la que tenés que repasar. Y además te va a
servir para la entrega.
Están ordenados de menor a mayor dificultad. No hace falta hacerlos todos.
Vale mucho más hacer dos bien y poder explicarlos, que hacer cinco a medias. Y antes de empezar cualquiera, hacé lo de siempre aunque sea en dos líneas: ¿qué objetos aparecen? ¿qué información tienen? ¿qué pueden hacer? ¿cómo se relacionan?
mostrar_info() a Arma y otro a Enemigo.Qué practicás: clase vs objeto · atributos · métodos · print() vs
return.
La pregunta: mostrar_info(), ¿usa print() o return? ¿Por qué?
curarse().vida_maxima. Probalo curándote con la vida casi llena.usos que baje cada vez y avise cuando se acabó.Qué practicás: composición (TIENE UN) · encapsulación · un objeto le pregunta a otro.
La pregunta: el control de la vida máxima, ¿va adentro de curarse() o afuera, en el
menú? ¿Quién tiene que decidir cómo cambia la vida del jugador?
Jugador, cada uno con un atributo propio.atacar(): el guerrero suma su fuerza, el mago su magia, el arquero pega doble cada dos turnos.for. Sin un solo if.Qué practicás: herencia de varios niveles · super() · polimorfismo.
La pregunta: cuando agregaste el tercer personaje al for, ¿tuviste que cambiar el
for? Esa es exactamente la ventaja del polimorfismo.
agregar(), mostrar() y cantidad(). Decidí cuál usa return.Qué practicás: composición con listas · objetos adentro de objetos · return.
La pregunta: el jugador tiene un inventario, y el inventario tiene armas. ¿Cómo se lee
jugador.inventario.armas[0].nombre?
comprar() que revise si al jugador le alcanza. Ese método tiene que devolver algo.for.Enemigo) con un ataque especial.Qué practicás: todo junto · varios objetos colaborando · herencia + composición +
polimorfismo · diseño.
La pregunta: el for que recorre los enemigos, ¿necesita saber si el que viene es un
goblin, un dragón o el jefe final?
No agregues clases porque sí. Si creás Moneda, PuntoDeVida y
NumeroDeTurno como clases, volviste al desorden del principio. Preguntate siempre:
¿esto tiene información propia y hace algo? Si no, es un atributo de alguien.
Estos errores los vas a cometer sí o sí. Lo importante es reconocer el síntoma para saber enseguida qué te está pasando.
super()AttributeError: … has no attribute 'nombre'
El padre nunca hizo su parte, así que ese atributo no existe. Andá al __init__ del
hijo y fijate si está la llamada al padre, como primera línea.
print donde iba returnAparece None donde esperabas un número
El método mostró el valor pero no lo entregó. Cambiá print por return y
volvé a probar. Si el while de la pelea no corta nunca, casi seguro es esto.
"un jugador es un arma" 🤨
Si la frase "es un" suena rara al leerla en voz alta, no era herencia: era tiene un. Sacá el
(Arma) y poné self.arma = arma.
self.El dato "desaparece" después del __init__
Si escribiste vida = vida en vez de self.vida = vida, el dato no se guardó
en el objeto: quedó en una variable que murió ahí. Es el error número uno de la clase 1.
El menú no entra en ninguna opción
input() devuelve texto. Hay que comparar con opcion == "1", entre
comillas, y no con opcion == 1.
IndentationError · el método quedó afuera
Los métodos van indentados adentro de la clase. Si un def quedó pegado al
margen, dejó de ser un método y pasó a ser una función suelta.
¿Esto lo tienen todos o lo tiene uno solo? Si lo tienen todos, va en la clase de arriba. Si lo tiene uno solo, va en el hijo.
Leé las dos frases en voz alta: "un X es un Y" y "un X tiene un Y". La que suena natural es la correcta.
¿Lo necesita una persona o lo necesita el programa? Persona → print().
Programa → return.
Leela de derecha a izquierda poniendo un "de" en cada punto:
jugador.arma.nombre es "el nombre del arma del jugador".
1) ¿Qué objetos hay acá? · 2) ¿Qué debería tener la clase de arriba? · 3) ¿Qué agrega cada hijo? ·
4) ¿Dónde usamos super()? · 5) ¿Qué objeto tiene a otro adentro? · 6) ¿Qué método debería
devolver un valor? · 7) ¿Dónde herencia y dónde composición?
Cada estudiante o pareja (máximo dos personas) entrega estas seis cosas.
juego.py, que tiene que
ejecutarse sin errores. Probalo de cero antes de entregar.super(), composición, polimorfismo, return y encapsulación.Los dos tienen que poder explicar todo el código, no solo la parte que escribieron. En la defensa se le pregunta a cualquiera de los dos, sobre cualquier parte.
Se evalúa principalmente el razonamiento y la comprensión, no la cantidad de funcionalidades. Un juego chiquito que entendés a fondo vale más que uno enorme que no sabés explicar.
| Qué se mira | Lo esperado |
|---|---|
| Identificación de objetos | Encontró los objetos del problema y no convirtió en clase cualquier cosa que se le ocurrió. |
| Diseño de clases | Lo común está arriba, lo específico abajo. Cada clase tiene un motivo para existir. |
| Atributos y métodos | Los atributos son datos y están en la clase correcta; los métodos son acciones y hacen una cosa cada uno. |
| Herencia | Se usa donde se puede decir "ES UN", y no para ahorrar líneas porque sí. |
Uso de super() | El hijo no repite los atributos del padre. Puede explicar qué pasa si lo saca. |
| Composición | Hay al menos un objeto adentro de otro, y sabe justificar por qué ahí no iba herencia. |
| Polimorfismo | Distintos objetos responden al mismo método a su manera, y el código que los usa no tiene cadenas de if. |
Uso de return | Devuelve cuando el programa necesita el valor; imprime cuando lo lee una persona. Puede explicar cada caso. |
| Funcionamiento | El programa corre sin errores y hace lo que dice que hace. |
| Capacidad de explicar las decisiones | Lo que más pesa. Puede contar por qué diseñó así y qué otra opción había. |
| Prolijidad y mejoras | Nombres claros, código ordenado, y una idea concreta de cómo seguiría. |
Que recites la definición de polimorfismo. Lo que importa es que, frente a un problema, sepas encontrar los objetos y hacerles las dos preguntas. El resto es vocabulario y se aprende usándolo.
Marcá cada casilla solo si podés explicarlo en voz alta, sin leer. Si alguna te queda sin marcar, volvé a esa sección: ahí está lo que te falta.
super() y qué pasaría si lo saco.return y por qué esos y no otros.Para cada par, decidí si va herencia o composición. Leelos en voz alta.
Escribí en qué clase de nuestro juego tiene que estar cada uno, y por qué:
nombre · vida · experiencia · arma ·
danio (el de la espada) · danio (el del goblin) · vida_maxima.
espada = Arma("Espada", 25) aria = Jugador("Aria", 100, espada) goblin = Goblin("Goblin", 60, 12) goblin.recibir_danio(aria.atacar()) print(goblin.vida) goblin.recibir_danio(aria.atacar()) goblin.recibir_danio(aria.atacar()) print(goblin.vida) print(goblin.esta_vivo())
Escribí qué imprime cada print. Hacé las cuentas a mano.
class Enemigo(Personaje): def __init__(self, nombre, vida, danio): danio = danio def atacar(self): print(self.danio) goblin = Enemigo("Goblin", 60, 12) print(goblin.nombre) danio = goblin.atacar() print(danio + 5)
Hay tres cosas mal. Encontralas, decí qué error da cada una y arreglalas.
Sin escribir código: diseñá el sistema de una veterinaria usando las 7 preguntas del diseño. Entregá un dibujo con cajas y flechas. Tienen que aparecer al menos una herencia, una composición y un método que devuelva un valor.
Este ejercicio parece que no tiene nada que ver con el trabajo práctico. Es el más importante de todos: si te sale, entendiste que POO no era sobre videojuegos.
Si mañana te damos un problema completamente diferente —una veterinaria, un cine, una app de música—, ¿qué deberías preguntarte antes de escribir código? Escribilo con tus palabras.
Están acá al final a propósito: si las tenés al lado del enunciado, el ojo las lee antes de que la cabeza intente. Resolvé primero, aunque te salga mal — equivocarte y después entender por qué es la parte que te lo deja aprendido.
En varios ejercicios no hay una única respuesta correcta. Si la tuya es distinta pero pasa el test de leerla en voz alta, está bien.
Sí son objetos: el héroe, el goblin, la espada y la poción. Los cuatro guardan información y hacen algo.
No son objetos: la barra de vida (es la forma de mostrar un dato; la vida es un atributo del personaje), los 40 puntos de experiencia (un número que guarda el jugador: atributo suyo) y el fondo (no tiene información ni hace nada: es decoración).
Atributos: nombre, vida, experiencia — son
sustantivos, cosas que el héroe tiene.
Métodos: atacar(), recibir_danio(), subir_nivel() — son
verbos con paréntesis, cosas que el héroe hace.
| Clase | Atributos | Métodos |
|---|---|---|
| Personaje | nombre, vida | recibir_danio(), esta_vivo() |
| Jugador | nivel, experiencia, arma | atacar(), ganar_experiencia() |
| Enemigo | danio | atacar() |
| Arma | nombre, danio | usar() |
En Jugador y en Enemigo no se repiten nombre ni vida: eso ya está en
Personaje y lo heredan. Si agregaste vida_maxima o mostrar_info(), también
está bien.
recibir_danio()1. Quedan 70. 2. Devuelve False: la cuenta 0 > 0 da falso.
3. La vida queda en −400, y por eso hay que arreglarlo:
def recibir_danio(self, cantidad): self.vida = self.vida - cantidad if self.vida < 0: self.vida = 0 # nunca menos de 0
Ese if es la única razón por la que la vida no queda en negativo. Si lo sacás, el pilar
de la encapsulación desaparece.
espada.usar() → 25 · arco.usar() → 15 ·
espada.nombre → Espada · resultado - 5 → 20.
¿print o return? 1. return (el daño se lo restamos a la vida del enemigo).
2. print() (lo lee una persona). 3. return (la respuesta va adentro de un
if). 4. print() (nadie lo reutiliza). 5. return (con ese número
después decidís si sube de nivel).
super() y ES UN / TIENE UNEl error: AttributeError: 'Jugador' object has no attribute 'nombre'. Falta
super().__init__(nombre, vida) como primera línea: el padre nunca hizo su parte, así
que nombre jamás se guardó. Heredar la clase no alcanza: hay que llamar al
__init__ del padre.
Los pares: 1. ES UN · 2. TIENE UN · 3. TIENE UN · 4. ES UN · 5. TIENE UN · 6. ES UN · 7. TIENE UN · 8. ES UN.
Funcionan: goblin.nombre y goblin.vida (los puso Personaje a través
del super()), goblin.danio (propio de Enemigo), y
goblin.esta_vivo() y goblin.recibir_danio(20) — aunque no estén escritos
adentro de Enemigo: los heredó.
Da error: goblin.arma. El arma es del Jugador, no de Personaje.
El Guerrero: imprime Aria, Espada, 8 y True. La que falla es
aria.danio: ninguna clase de la cadena guarda "danio" en el jugador — el daño lo tiene el
arma. Sería aria.arma.danio.
for polimórficoImprime Dragón 35 (20 + 15), Goblin 12 y Lobo 10. El +15 es solo
del dragón: está escrito adentro de Dragon, los goblins no lo heredan.
Y hay cero if en ese for: cada objeto sabe cómo responder.
1. El jugador ataca y obtenemos el daño · 2. El enemigo recibe ese daño · 3. Se muestra cómo quedó la vida del enemigo · 4. Si sigue vivo, el enemigo ataca · 5. El jugador recibe el daño del enemigo · 6. Se repite mientras los dos estén vivos · 7. Cuando uno cae, se muestra el ganador.
Y la difícil: atacar_a() devuelve el daño aunque ya se lo haya restado al otro
porque el golpe ya está dado, pero el programa todavía no sabe de cuánto fue. Con el
return podés mostrarlo, sumarlo a un total o guardar el récord del golpe más fuerte. Sin
él, esa información se pierde.
Imprime nivel 2 y experiencia 20. La cuenta: 60 + 60 = 120; como
120 >= 100, sube de nivel y le restamos 100, así que le quedan 20.
Usamos >= y no == porque con == solo subiría si cayera
justo en 100. Con 120 no subiría nunca.
1. 1) TIENE UN · 2) ES UN · 3) TIENE UN · 4) ES UN · 5) TIENE UN.
2. nombre, vida y vida_maxima → Personaje (los
comparten todos). experiencia y arma → Jugador (solo él sube de nivel
y solo él lleva arma). danio de la espada → Arma. danio del goblin →
Enemigo.
3. Primer print: 35 (60 − 25). Segundo: 0 — ojo, no −15, porque el
if de recibir_danio() lo frena en 0. Tercero: False.
4. Los tres errores: (a) falta super().__init__(nombre, vida), así que
goblin.nombre da AttributeError; (b) dice danio = danio en vez de
self.danio = danio, así que el dato no se guarda; (c) atacar() hace
print en vez de return, así que danio vale None y
danio + 5 revienta.
5. Una salida posible: herencia — Perro y Gato son ambos
Animal. Composición — un Turno tiene un Animal y un
Veterinario; una HistoriaClinica tiene una lista de
Consulta. Return — turno.precio() devuelve cuánto hay que cobrar. Y algo
que no va: Consultorio no hereda de Veterinaria — el consultorio está
adentro, no es una veterinaria.
6. No hay una única forma de decirlo, pero tienen que estar estas preguntas, en este orden: ¿Qué problema tengo? → ¿Qué objetos existen? → ¿Qué información tiene cada uno? → ¿Qué puede hacer? → ¿Cómo se relacionan? Y recién después, el código. Si tu respuesta empieza por "abro Python y…", volvé a leerla. Fijate que en ninguna de esas preguntas aparece la palabra Python: por eso el método te sirve para cualquier problema y en cualquier lenguaje.
Empezaste con una pantalla en blanco y terminaste con un videojuego funcionando. Pero lo que de verdad te llevás no es el juego: es que ahora, frente a cualquier problema, sabés por dónde empezar.
Y la primera pregunta va a seguir siendo siempre la misma: "¿qué objetos hay acá?" Y ahora, además, una segunda: "¿y cómo se relacionan entre ellos?"
¿Qué información tiene? → atributos (sustantivos).
¿Qué puede hacer? → métodos (verbos con paréntesis).
Si dudás, decilo en voz alta: "el personaje tiene…" / "el personaje puede…".
class Jugador(Personaje):
"Un jugador es un personaje." Una sola cosa que recibe todo lo del padre.
self.arma = arma
"Un jugador tiene un arma." Dos objetos distintos, conectados.
super().__init__(nombre, vida)
= "Padre, encargate vos de nombre y vida. Yo me ocupo del resto."
Va como primera línea del __init__ del hijo.
¿Lo necesita una persona o el programa?
print() → a la pantalla. Lo ve alguien y ahí termina.
return → me lo entrega. Lo guardo, lo comparo en un if, lo reutilizo.
Misma orden, distinta respuesta. Cada enemigo tiene su atacar().
El for que los recorre no tiene un solo if, y no cambia nunca
aunque agregues veinte enemigos.
jugador.arma.nombre
= "el nombre del arma del jugador". De derecha a izquierda.
Un atributo —y también un parámetro— puede ser otro objeto entero.
AttributeError →
falta el super().None →
print donde iba return.self.super()?El código está sexto. Cinco pasos antes de tocar el teclado. No memorices: razoná.