En éste ejercicio voy a proponer un proceso para construir algo con Python usando programación orientada a objetos (POO). Cuando digo proceso me refiero a un método de pasos a seguir, una hoja de ruta útil, que por supuesto se puede modificar y es susceptible de cualquier mejora. Como ya he dicho en alguna ocasión, no hay una única respuesta correcta, pero es interesante tener un método.
El proceso es el siguiente:
- Escribir o dibujar el problema
- Extraer y analizar los conceptos clave
- Crear un mapa de objetos y jerarquía de clases de los conceptos
- Escribir el código de las clases y hacer una prueba para ejecutarlas
- Repetir y refinar
Este proceso se realiza «de arriba a abajo», de lo abstracto a lo concreto, la implementación real.
ANÁLISIS DEL MOTOR DE UN JUEGO
El juego que haré se llamará «Gothons from Planet Percal #25» y será un pequeño juego de aventura espacial. Sin nada más que la idea en la mente, exploraré la idea y pensaré como hacer que el juego cobre vida.
1.- ESCRIBIR O DIBUJAR EL PROBLEMA
Voy a escribir un párrafo para el juego:
«Los aliens han invadido una nave espacial y nuestro héroe tiene que pasar por un laberinto de habitaciones derrotándolos, para que pueda escapar en una cápsula de escape hacia otro planeta. El juego será de tipo Zork o Adventure con salidas de texto y formas divertidas de morir. Incluirá un motor que ejecuta un completo mapa de habitaciones o escenas. Cada habitación mostrará su propia descripción cuando el jugador entre en ella e indicará al motor cuál es la próxima habitación del mapa a ejecutar.»
Describo cada escena:
- Death: Cuando el jugador muere. Debería ser algo divertido.
- Central corridor: Es el punto de partida y ya tiene un Gothon de pie, que el jugador tiene que derrotar con una broma antes de continuar.
- Laser weapon armony: Aquí es donde el héroe consigue una bomba de neutrones para explotar la nave antes de llegar a la cápsula de escape. Tiene un teclado y debe averiguar su código para usarlo.
- The bridge: Otra escena de batalla con un Gothon en la que el héroe coloca la bomba.
- Escape pod: Donde el héroe escapa, pero sólo despué de adivinar cuál es la cápsula de escape correcta.
Ahora puedo dibujar el mapa, incluso más descripciones de cada habitación (cualquier cosa que se me venga a la mente).
2.- EXTRAYENDO Y ANALIZANDO CONCEPTOS CLAVE
Tengo suficiente información para extraer algunos de los nombres y analizar la jerarquía de clases. Primero, crearé la jerarquía de clases:
- Alien
- Player
- Ship
- Maze (laberinto)
- Room
- Scene
- Gothon
- Escape pod (cápsula de escape)
- Planet
- Map
- Engine
- Death
- Central corridor
- Laser weapon armory (armería de armas láser)
- The bridge
También podría revisar los verbos y ver si podrían ser funciones de los nombres, pero de momento nos saltaremos esto.
3.- CREANDO UN MAPA DE OBJETOS Y UNA JERARQUÍA DE CLASES DE LOS CONCEPTOS
Mmmm. A ver. Tengo que construir una jerarquía de clases en base a similitudes y diferencias.
Room y Scene son básicamente lo mismo. Elijo Scene para éste juego porque Planet, Central corridor y Death son más escenas que habitaciones. Maze y Map son similares también, así que elijo Map ya que la usaré más. Alien y Player los guardaré para más adelante.
Entonces la jerarquía de clases es la siguiente:
- Map
- Engine
- Scene
- Death
- Central corridor
- Laser Weapon Armony
- The bridge
- Escape pod
Ahora voy a añadir verbos para las acciones que necesite…
- Map
- Next_scene
- opening_scene
- Engine
- play
- Scene
- enter
- Death
- Central corridor
- Laser Weapon Armony
- The bridge
- Escape pod
Añadimos enter debajo de Scene, ya que todas las ecenas hijas van a heredarlo y sobrescribirlo más tarde.
4.- ESCRIBIENDO EL CÓDIGO DE LAS CLASES Y UNA CLAVE PARA EJECUTARLAS
Una vez tengo el árbol de clases y algunas de las funciones, escribo el código con una pequeña prueba al final. Lo hago copiando el árbol anterior y convirtiéndolo en clases.
1 class Scene(object):
2
3 def enter(self):
4 pass
5
6
7 class Engine(object):
8
9 def __init__(self, scene_map):
10 pass
11
12 def play(self):
13 pass
14
15 class Death(Scene):
16
17 def enter(self):
18 pass
19
20 class CentralCorridor(Scene):
21
22 def enter(self):
23 pass
24
25 class LaserWeaponArmony(Scene):
26
27 def enter(self):
28 pass
29
30 class TheBridge(Scene):
31
32 def enter(self):
33 pass
34
35 class EscapePod(Scene):
36
37 def enter(self):
38 pass
39
40
41 class Map(object):
42
43 def __init__(self, start_scene):
44 pass
45
46 def next_scene(self, scene_name):
47 pass
48
49 def opening_scene(self):
50 pass
51
52
53 a_map = Map('central_corridor')
54 a_game = Engine(a_map)
55 a_game.play()
En éste archivo se puede ver que sólo replico la jerarquía que quería y luego añado un poco de código al final para ejecutarlo y comprobar que todo funciona. Avanzo y lo completeré.
5.- REPITIENDO Y REFINANDO…1,2,3…
El último paso del proceso es hacer un bucle while. No siempre éste proceso se hace en una sola vez.
Por lo general, hay que repetir el proceso de nuevo y cada vez que se refina basándose en la información aprendida en los pasos anteriores.
A veces, llegaré al paso 3 y me daré cuenta de que tengo que trabajar más el paso 1 y 2, por lo que pararé y volveré a trabajar en ellos… ¡¡¡un símil de éste blog!!! Y de la vida misma… XD
Otras veces, puedo tener un momento de inspiración y saltar al final para escribir el código que tengo en mente, pero después volveré atrás y seguiré los pasos para asegurarme de que se cubren todas las posibilidades que tengo.
Vamos, que el pensamiento lateral no es lineal precisamente. Y hay que saber organizarlo. Creo que ésto es importante.
La otra idea es que no es algo que se haga a un sólo nivel, sino que se puede hacer en todos los niveles cuando encuentro un problema en particular. Voy a suponer que todavía no se escribir el método Engine.play. Puedo parar y hacer todo éste proceso para averiguar cómo escribirlo.
DE ARRIBA A ABAJO VS DE ABAJO A ARRIBA
Lo que estaba haciendo hasta ahora es «de arriba a abajo», ya que parte de lo abstracto (parte superior) hasta llegar a la implementación real.
¡¡Pero hay otra forma muy interesante!!!
Éstos son los pasos:
- Tomar un fragmento pequeño del problema. Modificar algo del código…y conseguir que se ejecute.
- Refinar el código haciéndolo más formal, con clases y pruebas automatizadas.
- Extraer los conceptos clave utilizados, y tratar de saber más sobre ellos.
- Escribir lo que realmente está pasando.
- Volver atrás y refinar el código. Igual tengo que tirarlo a la basura y empezar de nuevo.. 😛
- Repetir el proceso pasando a algún otro fragmento del problema.
Éste método es muy bueno cuando conozco pequeñas piezas del rompecabezas en general, pero tal vez no tengo suficiente información sobre el concepto general. Modificando las pequeñas piezas y explorando el código, se consigue desgranar el problema, hasta conseguir resolverlo. Será más efectivo una vez voy cogiendo soltura en programación y piense con naturalidad el código del problema.
EL CÓDIGO: «GOTHONS FROM PLANET PERCAL #25»
Voy a partir el fichero ex43.py en partes para explicar cada una, en lugar de mostrar todo el código a la vez…
Éstos son los import para el juego:
from sys import exit from random import randit
Como se ve en el esqueleto del código, tengo una clase base para Scene, que tendrá las cosas comunes para las escenas. En éste programa tan simple, no son muchas, así que es más bien para ver cómo funciona la clase principal.
class Scene(object):
def enter(self):
print "This scene is not configured. Subclass it and implement enter()."
exit(1)
También tengo mi clase Engine y puedo ver como simplemente estoy usando los métodos Map.opening_scene y Map.next_scene. Como he hecho un poco de planificación se que tendré que escribirlos y luego usarlos antes de escribir la clase Map.
class Engine(object):
def __init__(self, scene_map):
self.scene_map = scene_map
def play(self):
current_scene = self.scene_map.opening_scene()
while True:
print "\n- - - - - - - - - "
next_scene_name = current_scene.enter()
current_scene = self.scene_map.next_scene(next_scene_name)
La primera escena es la llamada Death, que muestra el tipo de escena más sencilla de todas:
class Death(Scene):
quips = [
"You died. You kinda suck at this."
"Your mom would be proud...if she were smarter.",
"Such a luser.",
"I have a small puppy that's better at this."
]
def enter(self):
print Death.quips[randit(0, len(self.quips)-1)]
exit(1)
Después de ésto crearé el CentralCorridor, que el inicio del juego. Estoy haciendo las escenas para el juego antes que el Map porque necesito referencias después.
class CentralCorridor(Scene):
def enter(self):
print "The Gothons of Planet Percal #25 have invaded your ship and destroyed"
print "your entire crew. You are the last surviving member and your last"
print "mission is to get the neutron destruct bomb for the Weapons when"
print "a Gothon jumps out, red scaly skin, dark grimy teeth,and evil cloud costume"
print "flowing around his hated filled body. He's blocking the door to the"
print "Armory and about to pull a weapor to blast you."
action = raw_input ("> ")
if action == "shoot!!":
print "Quick on the draw you yank out your blaster and fire it at the Gothon."
print "His clown costume is flowing and moving around his body with throws"
print "off your aim. Your laser hits his costume but misses him entirely. This"
print "completely ruins his brand new costume his mother bought him, which"
print "makes him fly into a range and blast you repeatedly in the face until"
print "you are dead. Then he eats you."
return 'death'
elif action == "dodge!":
print "Like a world class boxer you dodge, weave, slip and slide right"
print "as the Gothon's blaster cranks a laser past your head."
print "In the middle of your artful dodge your foot slips and you"
print "bang your head on the metal wall and pass out."
print "You wake up shortly after only to die as the Gothon stomps on"
print "your head and eats you."
return 'death'
elif action == "tell a joke":
print "Lucky for you they made you learn Gothon insults in the academmy."
print "You telll the one Gothon joke you know:"
print "Lbne zbgure vf fb sng, jura fur fvgf nebhaq gur ubhfr, fur fvgf nebhaq gur ubhfr."
print "The Gothon stops, tries not to laugh, then busts out lauhing and can´t move."
print "While he's laughing you run up and shoot him square in the head"
print "putting him down, then jump though the Weapon Armory door."
return 'laser_weapon_armory'
else:
print "DOES NOR COMPUTE!"
return 'central_corridor'
A continuación, hago el resto de escenas del juego…
class LaserWeaponArmory(Scene):
def enter(self):
print "You do a dive roll into the Weapon Armory, crouch and scan the room"
print "for more Gothons that might be hiding. It's dead quiet. too quiet."
print "you stand up and run to the far side of the room and find the"
print "neutron bomb in its container. There's a keypad lock on the box"
print "and you need the code to get the bomb out. If you get the code"
print "wrong 10 times then the lock closes forever and you can't"
print "get the bomb. The code is 3 digits."
code = "%d%d%d" % (randint(1,9), randint(1,9), randint(1,9))
guess = raw_input("[keypad]> ")
guesses = 0
while guess != code and guesses < 10:
print "BZZZZED!"
guesses += 1
guess = raw_input("[keypad]> ")
if guess == code:
print "The container clicks open and the seal breaks, letting gas out."
print "You grab the neutron bomb and run as fast as you can to the"
print "bridge where you must place it in the right spot."
return 'the_bridge'
else:
print "The lock buzzes one last time and then you hear a sickening"
print "melting sound as the mecanism is fused together."
print "You decide to sit there, and finally the Gothons blow up the"
print "ship from their ship and you die."
return 'death'
class TheBridge(Scene):
def enter(self):
print "You burst onto the Bridge with the neutron destruct bomb"
print "under your arm and surprise 5 Gothons who are trying to"
print "take control of the ship. Each of them has an even uglier"
print "clown costume than the last. They haven't pulled their"
print "weapons out yet, as they see the active bomb under your"
print "arm and don't want to set it off."
action = raw_input("> ")
if action == "throw the bomb":
print "In a panic you throw the bomb at the group of Gothons"
print "and make a leap for the door. Right as ou drop it a"
print "Gothon shoots you right in the back killing you."
print "As you die you see another Gothon frantically try to disarm"
print "the bomb. You die knowing they will probably blow up when"
print "it goes off."
return 'death'
elif action == "slowly place the bomb":
print "You point your blaster at the bomb under your arm"
print "and the Gothons put their hands up and start to sweat."
print "You inch backward to the door, open it, and then carefully"
print "place the bomb on the floor, pointing your blaster at it."
print "You then jump back through the door, punch the close button"
print "and blast the lock so the Gothons can't get out."
print "Now that the bomb is placed you run to the escape pod to"
print "get off this tin can."
return 'escape_pod'
else:
print "DOES NOT COMPUTE!"
return "the_bridge"
class EscapePod(Scene):
def enter(self):
print "You rush through the ship desperately trying to make to"
print "the sscape pod before the whole ship explodes. It seems like"
print "hardly any Gothons are on the ship, so your run is clear of"
print "interference. You get to the chamber with the escape pods, and"
print "now need to pick one to take. Some of them could be damaged"
print "but you don't have time to look. There's 5 pods, which one"
print "do you take?"
good_pod = randint(1,5)
guess = raw_input("[pod #]> ")
if int(guess) != good_pod:
print "You jump into pod %s and hit the eject button." %guess
print "The pod escapes out into the void of space, then"
print "implodes as the hull ruptures, crushing your body"
print "into jam jelly."
return 'death'
else:
print "You jump into pod % and hit the eject button" %guess
print "The pod easily slides out into space heading to"
print "the planet below. As it flies to the planet, you look"
print "back and see your ship implode then explode like a"
print "bright star. taking out the Gothon ship at the same"
print "time. You won!"
return 'finished'
Después de ésto, tengo mi clase Map:
class Map(object):
scenes = [
'central_corridor': CentralCorridor(),
'laser_weapon_armory': LaserWeaponArmory(),
'the_bridge': TheBridge(),
'escape_pod': EscapePod(),
'death': Death()
]
def __init__(self, start_scene):
self.start_scene = start_scene
def next_scene(self, scene_name)
return Map.scenes.get(scene_name)
def opening_scene(self):
return self.next_scene(self.start_scene)
Se puede ver