RPG 3: el juego completo
En resumen: El RPG completo: una clase Game lleva al héroe por batallas, descanso, niveles y experiencia, y un movimiento incorrecto lanza GameError, que el juego captura para seguir adelante.
Problema
De una pelea a un juego
Los héroes y los monstruos ya luchan. Pero un juego es más que una pelea: creas un héroe, alineas los monstruos contra los que va a pelear, y los va superando uno por uno. Entre las peleas descansa o se cura con magia.
Si todo esto está en el programa principal, ahí estarán el héroe, una lista de monstruos, el número del siguiente, contadores y comprobaciones. ¿Y quién va a impedir que el jugador empiece una pelea sin héroe, o que le pida magia a un bárbaro?
El estado del juego y sus reglas tienen que estar en un solo lugar: en una clase Game.
La idea
La clase Game
El héroe y los monstruos son el estado del juego. Los métodos de Game son las reglas: qué puede hacer el jugador y qué pasa después. La primera es crear un héroe:
private Character hero;private Monster[] monstruos = new Monster[5];private int count;private int next;private Random random;
public Game(Random random) { this.random = random;}
public void createHero(String name, String race) { if (race.equals("paladín")) { hero = new Paladin(name); } else if (race.equals("bárbaro")) { hero = new Barbarian(name); } else if (race.equals("bruja")) { hero = new Witch(name); } else { throw new GameException("No existe esa raza: " + race); }}private Character hero;private Monster[] monstruos = new Monster[5];private int count;private int next;private Random random;
public Game(Random random){ this.random = random;}
public void CreateHero(string name, string race){ if (race == "paladín") { hero = new Paladin(name); } else if (race == "bárbaro") { hero = new Barbarian(name); } else if (race == "bruja") { hero = new Witch(name); } else { throw new GameException("No existe esa raza: " + race); }}def __init__(self, random): self._hero = None self._monsters = [None] * 5 self._count = 0 self._next = 0 self._random = random
def create_hero(self, name, race): if race == "paladín": self._hero = Paladin(name) elif race == "bárbaro": self._hero = Barbarian(name) elif race == "bruja": self._hero = Witch(name) else: raise GameError(f"No existe esa raza: {race}")#hero = null;#monsters = new Array(5);#count = 0;#next = 0;#random;
constructor(random) { this.#random = random;}
createHero(name, race) { if (race === "paladín") { this.#hero = new Paladin(name); } else if (race === "bárbaro") { this.#hero = new Barbarian(name); } else if (race === "bruja") { this.#hero = new Witch(name); } else { throw new GameError(`No existe esa raza: ${race}`); }}Random() sin semilla, y en los ejemplos es Random(12), para que la salida sea siempre la misma.Los campos empiezan con _: pertenecen al juego y no se tocan desde fuera. [None] * 5 es una lista con 5 lugares en los que todavía no hay nada (None).Una raza desconocida no es motivo para crear un héroe cualquiera. El método lanza GameError: nuestra propia excepción para cada movimiento que las reglas no permiten. Tiene su propia tarjeta más adelante.
La idea
Una fila de monstruos
Los monstruos esperan en una cola: el que se añadió primero es la primera pelea. Aquí la cola es una lista y dos índices:
public void addMonster(Monster monster) { if (count == monstruos.length) { throw new GameException("La fila está llena: como máximo " + monstruos.length + " monstruos"); } monstruos[count] = monster; count++;}
public Monster createRandomMonster() { int kind = random.nextInt(3); if (kind == 0) { return new FallenShaman(); } if (kind == 1) { return new SkeletonKing(); } return new Butcher();}
public boolean hasMonsters() { return next < count;}public void AddMonster(Monster monster){ if (count == monstruos.Length) { throw new GameException("La fila está llena: como máximo " + monstruos.Length + " monstruos"); } monstruos[count] = monster; count++;}
public Monster CreateRandomMonster(){ int kind = random.Next(3); if (kind == 0) { return new FallenShaman(); } if (kind == 1) { return new SkeletonKing(); } return new Butcher();}
public bool HasMonsters(){ return next < count;}def add_monster(self, monster): if self._count == len(self._monsters): raise GameError(f"La fila está llena: como máximo {len(self._monsters)} monstruos") self._monsters[self._count] = monster self._count += 1
def create_random_monster(self): kind = self._random.randrange(3) if kind == 0: return FallenShaman() if kind == 1: return SkeletonKing() return Butcher()
def has_monsters(self): return self._next < self._countaddMonster(monster) { if (this.#count === this.#monsters.length) { throw new GameError(`La fila está llena: como máximo ${this.#monsters.length} monstruos`); } this.#monsters[this.#count] = monster; this.#count++;}
createRandomMonster() { const kind = Math.floor(this.#random() * 3); if (kind === 0) { return new FallenShaman(); } if (kind === 1) { return new SkeletonKing(); } return new Butcher();}
hasMonsters() { return this.#next < this.#count;}_countes cuántos monstruos se han añadido. El nuevo va aself._monsters[self._count]._nextes cuál pelea a continuación. Después de cada pelea sube 1, y cuando llega a_count, la fila se acaba.
add_monster lanza GameError antes de escribir fuera de la lista. Una lista en Python también puede crecer con append, pero aquí deliberadamente no crece: la regla del juego es «como máximo 5».create_random_monster es el extra del ejercicio antiguo: un monstruo elegido al azar. randrange(3) da 0, 1 o 2.
Más precisamente, para curiosos
-
El ejercicio antiguo guardaba los monstruos en una cola de verdad. En Python es una
deque: recuerda por sí misma cuál es el primero, y nunca se llena. Llega en el módulo Colecciones: ahí desaparecen_county_next, y la fila crece lo que haga falta. - Los monstruos anteriores a
_nextse quedan en la lista. No hacen daño: nadie los lee ya.
La idea
Niveles y experiencia
Después de cada victoria el héroe recibe experiencia igual a la vida del monstruo. En el nivel 1 necesita 100 de experiencia para llegar al nivel 2, en el nivel 2 necesita 200 para llegar al 3, y así sucesivamente. Un nivel nuevo da 10 de vida más, 3 de fuerza más y la vida completa:
public boolean gainExperience(int exp) { experience += exp; boolean up = false; while (experience >= level * 100) { experience -= level * 100; levelUp(); up = true; } return up;}
public void levelUp() { level++; maxHealth += 10; power += 3; health = maxHealth;}public bool GainExperience(int exp){ Experience += exp; bool up = false; while (Experience >= Level * 100) { Experience -= Level * 100; LevelUp(); up = true; } return up;}
public void LevelUp(){ Level++; MaxHealth += 10; Power += 3; Health = MaxHealth;}def gain_experience(self, exp): self._experience += exp up = False while self._experience >= self._level * 100: self._experience -= self._level * 100 self.level_up() up = True return up
def level_up(self): self._level += 1 self._max_health += 10 self._power += 3 self._health = self._max_healthgainExperience(exp) { this._experience += exp; let up = false; while (this._experience >= this._level * 100) { this._experience -= this._level * 100; this.levelUp(); up = true; } return up;}
levelUp() { this._level++; this._maxHealth += 10; this._power += 3; this._health = this._maxHealth;}Los métodos están en Character. La experiencia que pasa del umbral se guarda para el nivel siguiente. El bucle while es para el caso en que una victoria alcanza para dos niveles.
Los campos _max_health, _level y _experience son nuevos en Character. Con _max_health, curar ya no sube la vida por encima del máximo. Desde fuera se leen con las propiedades max_health, level y experience, como los campos de la primera parte.
Pruébalo
¿En qué nivel está el bárbaro?
Grok empieza en el nivel 1 con fuerza 20. Gana contra el chamán (60 de experiencia) y contra el rey (90 de experiencia).
¿Qué imprimirá el programa? Nivel, experiencia, fuerza.
def gain_experience(self, exp): self._experience += exp up = False while self._experience >= self._level * 100: self._experience -= self._level * 100 self.level_up() up = True return up
def level_up(self): self._level += 1 self._max_health += 10 self._power += 3 self._health = self._max_health
grok = Barbarian("Grok")grok.gain_experience(60)grok.gain_experience(90)print(grok.level, grok.experience, grok.power)60 + 90 = 150 de experiencia. En el nivel 1 el umbral es 100: Grok sube al nivel 2, y sobran 50. El nivel 3 cuesta 200. La fuerza es 20 + 3 = 23.
Míralo
La batalla y el descanso
start_battle primero comprueba si la pelea es posible. Después toma el siguiente monstruo y ejecuta la pelea a muerte de la segunda parte. Devuelve si el héroe sobrevivió:
public boolean startBattle() { if (hero == null) { throw new GameException("Crea primero un héroe"); } if (!hasMonsters()) { throw new GameException("No hay monstruos en la fila"); } Monster monster = monstruos[next]; next++; int rounds = 0; while (hero.isAlive() && monster.isAlive()) { rounds++; hero.attack(monster); if (monster.isAlive()) { monster.attack(hero); } } if (!hero.isAlive()) { System.out.println(hero.getName() + " cae en la ronda " + rounds + ". Ganador: " + monster.getName()); return false; } System.out.println(monster.getName() + " es derrotado en " + rounds + " rondas. " + hero.getName() + ": vida " + hero.getHealth() + "/" + hero.getMaxHealth()); if (hero.gainExperience(monster.getReward())) { System.out.println(hero.getName() + " llega al nivel " + hero.getLevel() + ": vida " + hero.getMaxHealth() + ", fuerza " + hero.getPower()); } return true;}public bool StartBattle(){ if (hero == null) { throw new GameException("Crea primero un héroe"); } if (!HasMonsters()) { throw new GameException("No hay monstruos en la fila"); } Monster monster = monstruos[next]; next++; int rounds = 0; while (hero.IsAlive() && monster.IsAlive()) { rounds++; hero.Attack(monster); if (monster.IsAlive()) { monster.Attack(hero); } } if (!hero.IsAlive()) { Console.WriteLine(hero.Name + " cae en la ronda " + rounds + ". Ganador: " + monster.Name); return false; } Console.WriteLine(monster.Name + " es derrotado en " + rounds + " rondas. " + hero.Name + ": vida " + hero.Health + "/" + hero.MaxHealth); if (hero.GainExperience(monster.Reward)) { Console.WriteLine(hero.Name + " llega al nivel " + hero.Level + ": vida " + hero.MaxHealth + ", fuerza " + hero.Power); } return true;}def start_battle(self): if self._hero is None: raise GameError("Crea primero un héroe") if not self.has_monsters(): raise GameError("No hay monstruos en la fila") hero = self._hero monster = self._monsters[self._next] self._next += 1 rounds = 0 while hero.is_alive() and monster.is_alive(): rounds += 1 hero.attack(monster) if monster.is_alive(): monster.attack(hero) if not hero.is_alive(): print(f"{hero.name} cae en la ronda {rounds}. Ganador: {monster.name}") return False print(f"{monster.name} es derrotado en {rounds} rondas. {hero.name}: vida {hero.health}/{hero.max_health}") if hero.gain_experience(monster.reward): print(f"{hero.name} llega al nivel {hero.level}: vida {hero.max_health}, fuerza {hero.power}") return TruestartBattle() { if (this.#hero === null) { throw new GameError("Crea primero un héroe"); } if (!this.hasMonsters()) { throw new GameError("No hay monstruos en la fila"); } const hero = this.#hero; const monster = this.#monsters[this.#next]; this.#next++; let rounds = 0; while (hero.isAlive() && monster.isAlive()) { rounds++; hero.attack(monster); if (monster.isAlive()) { monster.attack(hero); } } if (!hero.isAlive()) { console.log(`${hero.name} cae en la ronda ${rounds}. Ganador: ${monster.name}`); return false; } console.log(`${monster.name} es derrotado en ${rounds} rondas. ${hero.name}: vida ${hero.health}/${hero.maxHealth}`); if (hero.gainExperience(monster.reward)) { console.log(`${hero.name} llega al nivel ${hero.level}: vida ${hero.maxHealth}, fuerza ${hero.power}`); } return true;}recover es la elección después de la pelea. Solo un héroe que hereda de Healable (lo comprueba isinstance) puede usar magia. El descanso son 5 turnos de 10 de vida:
public void recover(String way) { if (way.equals("descanso")) { for (int turn = 0; turn < 5; turn++) { hero.restore(10); } System.out.println(hero.getName() + " descansa 5 turnos: vida " + hero.getHealth() + "/" + hero.getMaxHealth()); } else if (way.equals("magia")) { if (!(hero instanceof IHealable)) { throw new GameException(hero.getName() + " no domina la magia curativa"); } System.out.println(hero.useAbility()); } else { throw new GameException("Elección desconocida «" + way + "»: ¿descanso o magia?"); }}public void Recover(string way){ if (way == "descanso") { for (int turn = 0; turn < 5; turn++) { hero.Restore(10); } Console.WriteLine(hero.Name + " descansa 5 turnos: vida " + hero.Health + "/" + hero.MaxHealth); } else if (way == "magia") { if (!(hero is IHealable)) { throw new GameException(hero.Name + " no domina la magia curativa"); } Console.WriteLine(hero.UseAbility()); } else { throw new GameException("Elección desconocida «" + way + "»: ¿descanso o magia?"); }}def recover(self, way): hero = self._hero if way == "descanso": for _ in range(5): hero.restore(10) print(f"{hero.name} descansa 5 turnos: vida {hero.health}/{hero.max_health}") elif way == "magia": if not isinstance(hero, Healable): raise GameError(f"{hero.name} no domina la magia curativa") print(hero.use_ability()) else: raise GameError(f"Elección desconocida «{way}»: ¿descanso o magia?")recover(way) { const hero = this.#hero; if (way === "descanso") { for (let turn = 0; turn < 5; turn++) { hero.restore(10); } console.log(`${hero.name} descansa 5 turnos: vida ${hero.health}/${hero.maxHealth}`); } else if (way === "magia") { if (typeof hero.heal !== "function") { throw new GameError(`${hero.name} no domina la magia curativa`); } console.log(hero.useAbility()); } else { throw new GameError(`Elección desconocida «${way}»: ¿descanso o magia?`); }}En el ejercicio antiguo el héroe esperaba 5 segundos. Aquí el descanso se cuenta en turnos, porque en un juego por turnos el tiempo se mide en turnos, y la comprobación del juego no debería tener que esperar.
Míralo
El juego completo
Grok el bárbaro contra el chamán y dos monstruos aleatorios. Después de cada pelea descansa. Si cae, el juego termina:
Game game = new Game(new Random(12));game.createHero("Grok", "bárbaro");game.addMonster(new FallenShaman());game.addMonster(game.createRandomMonster());game.addMonster(game.createRandomMonster());while (game.hasMonsters()) { if (!game.startBattle()) { System.out.println("Fin del juego"); break; } game.recover("descanso");}Chamán Caído es derrotado en 3 rondas. Grok: vida 90/110Grok descansa 5 turnos: vida 110/110Chamán Caído es derrotado en 3 rondas. Grok: vida 90/110Grok llega al nivel 2: vida 120, fuerza 23Grok descansa 5 turnos: vida 120/120Carnicero es derrotado en 6 rondas. Grok: vida 70/120Grok descansa 5 turnos: vida 120/120Game game = new Game(new Random(12));game.CreateHero("Grok", "bárbaro");game.AddMonster(new FallenShaman());game.AddMonster(game.CreateRandomMonster());game.AddMonster(game.CreateRandomMonster());while (game.HasMonsters()){ if (!game.StartBattle()) { Console.WriteLine("Fin del juego"); break; } game.Recover("descanso");}Chamán Caído es derrotado en 3 rondas. Grok: vida 90/110Grok descansa 5 turnos: vida 110/110Carnicero es derrotado en 7 rondas. Grok: vida 48/110Grok llega al nivel 2: vida 120, fuerza 23Grok descansa 5 turnos: vida 120/120Rey Esqueleto es derrotado en 5 rondas. Grok: vida 68/120Grok descansa 5 turnos: vida 118/120game = Game(Random(12))game.create_hero("Grok", "bárbaro")game.add_monster(FallenShaman())game.add_monster(game.create_random_monster())game.add_monster(game.create_random_monster())while game.has_monsters(): if not game.start_battle(): print("Fin del juego") break game.recover("descanso")Chamán Caído es derrotado en 3 rondas. Grok: vida 90/110Grok descansa 5 turnos: vida 110/110Rey Esqueleto es derrotado en 6 rondas. Grok: vida 45/110Grok llega al nivel 2: vida 120, fuerza 23Grok descansa 5 turnos: vida 120/120Rey Esqueleto es derrotado en 5 rondas. Grok: vida 68/120Grok descansa 5 turnos: vida 118/120const game = new Game(makeRandom(12));game.createHero("Grok", "bárbaro");game.addMonster(new FallenShaman());game.addMonster(game.createRandomMonster());game.addMonster(game.createRandomMonster());while (game.hasMonsters()) { if (!game.startBattle()) { console.log("Fin del juego"); break; } game.recover("descanso");}Chamán Caído es derrotado en 3 rondas. Grok: vida 90/110Grok descansa 5 turnos: vida 110/110Rey Esqueleto es derrotado en 6 rondas. Grok: vida 45/110Grok llega al nivel 2: vida 120, fuerza 23Grok descansa 5 turnos: vida 120/120Rey Esqueleto es derrotado en 5 rondas. Grok: vida 68/120Grok descansa 5 turnos: vida 118/120game = Game(Random(12))game.create_hero("Grok", "bárbaro")game.add_monster(FallenShaman())game.add_monster(game.create_random_monster())game.add_monster(game.create_random_monster())while game.has_monsters(): if not game.start_battle(): print("Fin del juego") break game.recover("descanso")Paso a paso, en palabras
- El juego recibe un generador con semilla 12. El héroe es Grok, y la lista tiene 5 lugares, todavía vacíos.
- Cada add_monster() escribe en self._monsters[self._count] y sube self._count. El segundo y el tercero son aleatorios: Rey Esqueleto y Rey Esqueleto.
- self._next vale 0: sale Chamán Caído. Es derrotado en 3 rondas, y a Grok le quedan 90/110.
- self._next pasa a 1. El descanso son 5 turnos de 10 de vida: 110/110.
- self._next vale 1: sale Rey Esqueleto. Es derrotado en 6 rondas, y a Grok le quedan 45/110. La experiencia alcanza para el nivel 2: vida 120, fuerza 23.
- self._next pasa a 2. El descanso son 5 turnos de 10 de vida: 120/120.
- self._next vale 2: sale Rey Esqueleto. Es derrotado en 5 rondas, y a Grok le quedan 68/120.
- self._next pasa a 3. El descanso son 5 turnos de 10 de vida: 118/120.
- self._next vale 3, igual que self._count: has_monsters() devuelve False, y el bucle termina.
La idea
Un movimiento incorrecto: GameError
El jugador puede pedir una pelea sin héroe, una raza que no existe, o magia a un bárbaro. Eso no es un error del programa. Es un movimiento que las reglas no permiten. Los métodos de Game lanzan para eso su propia excepción:
class GameException extends RuntimeException { public GameException(String message) { super(message); }}class GameException : Exception{ public GameException(string message) : base(message) { }}class GameError(Exception): passclass GameError extends Error { constructor(message) { super(message); this.name = "GameError"; }}Error, como la mayoría de las incorporadas: ValueError, TypeError, IndexError.El juego captura solo esa, le dice al jugador por qué el movimiento no está permitido y sigue con el movimiento siguiente:
Game game = new Game(new Random(12));String[] commands = { "pelea", "héroe Grok dragón", "héroe Grok bárbaro", "magia", "dormir" };for (String command : commands) { String[] words = command.split(" "); try { if (words[0].equals("pelea")) { game.startBattle(); } else if (words[0].equals("héroe")) { game.createHero(words[1], words[2]); System.out.println("El héroe es " + words[1]); } else { game.recover(words[0]); } } catch (GameException e) { System.out.println("No se puede: " + e.getMessage()); }}No se puede: Crea primero un héroeNo se puede: No existe esa raza: dragónEl héroe es GrokNo se puede: Grok no domina la magia curativaNo se puede: Elección desconocida «dormir»: ¿descanso o magia?Game game = new Game(new Random(12));string[] commands = { "pelea", "héroe Grok dragón", "héroe Grok bárbaro", "magia", "dormir" };foreach (string command in commands){ string[] words = command.Split(' '); try { if (words[0] == "pelea") { game.StartBattle(); } else if (words[0] == "héroe") { game.CreateHero(words[1], words[2]); Console.WriteLine("El héroe es " + words[1]); } else { game.Recover(words[0]); } } catch (GameException e) { Console.WriteLine("No se puede: " + e.Message); }}No se puede: Crea primero un héroeNo se puede: No existe esa raza: dragónEl héroe es GrokNo se puede: Grok no domina la magia curativaNo se puede: Elección desconocida «dormir»: ¿descanso o magia?game = Game(Random(12))commands = ["pelea", "héroe Grok dragón", "héroe Grok bárbaro", "magia", "dormir"]for command in commands: words = command.split() try: if words[0] == "pelea": game.start_battle() elif words[0] == "héroe": game.create_hero(words[1], words[2]) print(f"El héroe es {words[1]}") else: game.recover(words[0]) except GameError as e: print(f"No se puede: {e}")No se puede: Crea primero un héroeNo se puede: No existe esa raza: dragónEl héroe es GrokNo se puede: Grok no domina la magia curativaNo se puede: Elección desconocida «dormir»: ¿descanso o magia?const game = new Game(makeRandom(12));const commands = ["pelea", "héroe Grok dragón", "héroe Grok bárbaro", "magia", "dormir"];for (const command of commands) { const words = command.split(" "); try { if (words[0] === "pelea") { game.startBattle(); } else if (words[0] === "héroe") { game.createHero(words[1], words[2]); console.log(`El héroe es ${words[1]}`); } else { game.recover(words[0]); } } catch (e) { if (!(e instanceof GameError)) { throw e; } console.log(`No se puede: ${e.message}`); }}No se puede: Crea primero un héroeNo se puede: No existe esa raza: dragónEl héroe es GrokNo se puede: Grok no domina la magia curativaNo se puede: Elección desconocida «dormir»: ¿descanso o magia?¿Por qué una excepción propia y no la incorporada ValueError? Porque except GameError captura solo los rechazos según las reglas. Si el juego tiene un error y lanza AttributeError, no se esconderá como «No se puede». Detendrá el programa, y lo verás. Por ejemplo, recover antes de que haya un héroe: self._hero es None, y hero.restore(10) da AttributeError: 'NoneType' object has no attribute 'restore'.
Pruébalo
Encuentra el error
El sexto monstruo debería recibir «La fila está llena». En cambio, el programa se detiene con IndexError. ¿En qué línea está el error?
Elige la línea con el error.
self._count vale 5, y 5 > len(self._monsters) es False. La comprobación lo deja pasar, y el sexto se escribe en self._monsters[5], cuando el último lugar es self._monsters[4]: IndexError: list assignment index out of range. La lista está llena cuando self._count se iguala a su longitud: self._count == len(self._monsters).Pruébalo
¿Qué falta?
¿De qué clase hereda GameError?
class GameError():
passException, el padre de las excepciones propias en Python. Sin un __init__ propio, GameError usa el constructor de Exception: guarda el mensaje, y str(e) lo devuelve.Ejercicio
Juega y amplía
Resuelve estos ejercicios en tu editor. Empieza con las clases de la lección.
1. Un menú. Los comandos llegan por la entrada, uno por línea, hasta «salir»:
- «héroe Nombre raza» crea el héroe;
- «monstruo chamán» (o «rey», «carnicero») añade un monstruo ya hecho, y «monstruo Nombre vida fuerza» añade uno tuyo: una clase
Beastde la raza «bestia»; - «azar» añade un monstruo aleatorio;
- «pelea» empieza la siguiente pelea, y «descanso» y «magia» son la elección después de ella.
Cada rechazo es una GameError que el menú captura, diciendo «No se puede: …». La semilla es 12. Las líneas se leen con input().
héroe Morena elfohéroe Morena brujamonstruo Lobo 50 9monstruo dragónmonstruo chamánazarpeleamagiapeleadescansopeleasalirNo se puede: No existe esa raza: elfoEl héroe es MorenaEn la fila: LoboNo se puede: No existe ese monstruo: dragónEn la fila: Chamán CaídoEn la fila: Rey EsqueletoLobo es derrotado en 4 rondas. Morena: vida 68/95Morena invoca a los elementos: vida 83/95Chamán Caído es derrotado en 4 rondas. Morena: vida 47/95Morena llega al nivel 2: vida 105, fuerza 19Morena descansa 5 turnos: vida 105/105Rey Esqueleto es derrotado en 7 rondas. Morena: vida 9/1052. Progreso. Añade a Game un método show_progress() que muestre el nivel del héroe y cuánta experiencia tiene de la que necesita para el siguiente nivel. Ardan el paladín pasa por dos chamanes y el Rey Esqueleto. Después de cada pelea muestra su progreso y se cura con magia.
Chamán Caído es derrotado en 5 rondas. Ardan: vida 72/120Nivel 1, experiencia 60/100Ardan se cura: vida 92/120Chamán Caído es derrotado en 5 rondas. Ardan: vida 44/120Ardan llega al nivel 2: vida 130, fuerza 17Nivel 2, experiencia 20/200Ardan se cura: vida 130/130Rey Esqueleto es derrotado en 8 rondas. Ardan: vida 18/130Nivel 2, experiencia 110/200Ardan se cura: vida 38/1303. Un jefe. Una clase Diablo, subclase de Monster: 160 de vida, 14 de fuerza. Uno de cada tres golpes suyos es doble. Un método can_challenge(hero) dice si el héroe está al menos en el nivel 2. Si no, start_battle lanza GameError, y Diablo se queda en la fila. Ejecútalo una vez contra Grok en el nivel 1, y luego en un juego nuevo después del chamán y del rey.
No se puede: Diablo solo pelea contra héroes de nivel 2 o másChamán Caído es derrotado en 3 rondas. Grok: vida 90/110Grok descansa 5 turnos: vida 110/110Rey Esqueleto es derrotado en 6 rondas. Grok: vida 45/110Grok llega al nivel 2: vida 120, fuerza 23Grok descansa 5 turnos: vida 120/120Diablo es derrotado en 7 rondas. Grok: vida 24/120Grok llega al nivel 3: vida 130, fuerza 26Grok descansa 5 turnos: vida 130/130Sigue por tu cuenta
El ejercicio antiguo proponía más ideas. Algunas necesitan una herramienta que llega más adelante:
- Habilidades con niveles. Una clase
Skillcon un nombre y un nivel,use_skill(target)yupgrade_skill(). Esto ya puedes hacerlo, con una lista de habilidades en el héroe. - Inventario. Una clase
Itemcon un nombre y un efecto,apply_effect(target), y unInventorydonde se añaden y se quitan objetos. Añadir funciona incluso ahora: una lista crece conappend. Quitar un elemento del medio llega en el módulo Colecciones. - Jefes con requisitos. Un jefe que pide ciertos objetos y habilidades antes de aceptar la pelea, y suelta botín después de una victoria. Necesita el inventario de arriba.
- Pantallas. Menú, mapa, inventario: cada pantalla es un objeto con
show(),hide()yupdate(), y las pantallas abiertas están en una pila, para que «volver» funcione. La pila llega en el módulo Colecciones. - Guardar y cargar. El héroe, el nivel y la mejor puntuación se escriben en un archivo y se leen en la siguiente ejecución. Los archivos llegan en el módulo Texto y archivos.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Interfaces como clases abstractas · Comprobaciones de tipo · Excepciones · Lanzar excepciones y crear las tuyas
- Ejercicios sobre el tema: Hagamos un juego