En esta 2ª Competición Preparatoria para el CodedArena Challenge Tournament, hemos asistido a una emocionante partida entre los personajes Aina, del equipo CM2 del Colegio San Francisco de Paula, y MartaF, de Life&Co del Col·legi Montserrat.

Esta partida nos da pié a explicar un concepto muy interesante que nos permite CodedArena, y la programación el Python: los combos de hechizos.

¿Qué es un combo de hechizos?

Un combo es una secuencia ordenada de hechizos. Lanzar un conjunto de hechizos, siguiendo una estrategia lógica, del inicio al final.

via GIPHY

¿Y eso no es como simplemente poner un condicional debajo de otro, todo el rato? Pues... ¡NO!

¡Veamos porqué!

Programación cuyo resultado no te esperas

Siempre que creemos código en Python en CodedArena para nuestro personaje, es muy importante recordar que se ejecuta constantemente, de arriba hacia abajo, tal y como explica este vídeo del canal de YouTube de CodedArena.

Es decir, cada vez que vuestro personaje finaliza una acción (lanzar un hechizo, un movimiento, etc), el juego vuelve a preguntarle qué quiere hacer.

Pero... entre acción y acción, ha pasado tiempo... y el tiempo es muy importante en CodedArena.

¡Nuestra estrategia infalible!

Imaginemos que queremos crear una estrategia para nuestro personaje, que vaya atacando pero que, en caso de detectar que está bajo de vida, se cure y se ponga un escudo.

Simplificando el código (no tendremos en cuenta las distancias), podría ser una secuencia de este tipo:

if self.is_ready("lightning") and self.cost_of("lightning") < self.mana:
    return ["cast", "lightning", target]
if self.is_ready("corruption") and self.cost_of("corruption") < self.mana:
    return ["cast", "corruption", self]

if self.health < self.max_health * 0.45:
    if self.is_ready("heal") and self.cost_of("heal") < self.mana:
        return ["cast", "heal", self]
    if self.is_ready("shield") and self.cost_of("shield") < self.mana:
        return ["cast", "shield", self]

¡Fantástico! ¡Tenemos una estrategia que no puede fallar! Atacamos, y su detectamos que estamos bajos de vida, nos curamos y protegemos.

¡Un plan sin fisuras!

via GIPHY

Hasta que el paso del tiempo, y por lo tanto el enfriamiento de los hechizos, nos juega una mala pasada.

Y el resultado de nuestra programación no es el esperado.

Viendo nuestra partida

Miramos la reproducción de nuestra partida. Vemos que nuestro personaje, cuando empieza a perder vida, se cura, y a continuación... ¡va y lanza un rayo!

¡Noooo!¡No tenía que lanzar un rayo, tenía que ponerse un escudo!

¡Lo dice claramente nuestro código!

Pues no, no es cierto. El código explica una historia, pero esa historia está condicionada por la lógica. Lo que hace un segundo era False, quizá un segundo después es True.

El problema

Básicamente el problema es que en nuestra estrategia no hemos tenido en cuenta el tiempo de enfriamiento de los hechizos, es decir, el tiempo que pasa desde que nuestro personaje lanza un hechizo, y éste vuelve a estar disponible.

Cuando lanzamos un hechizo, y consultamos la función self.is_ready(), ésta devuelve False. Es decir, no está disponible porque se está enfriando.

Pero al cabo de unos segundos, dependiendo del hechizo, éste vuelve a estar disponible. Y, por lo tanto, self.is_ready() devuelve True.

Si miramos nuestro código, nos damos cuenta que el problema está en la primera línea:

if self.is_ready("lightning") and self.cost_of("lightning") < self.mana:

Lo que ha pasado, probablemente, es que mientras los hechizos Lightning y Corruption estaban enfriándose (cooldown), los dos primeros condicionales no se cumplieron (ya que self.is_ready()en ambos hechizos devolvía False). Así pues, la ejecución de nuestro programa llegó hasta la comparación de la vida, entró (porque nuestro personaje estaba bajo de vida), y ejecutó el hechizo Heal.

¿Y qué pudo pasar a continuación? ¿Por qué no entró en el condicional para lanzar Shield?

Pues porque el juego preguntó de nuevo al personaje qué quería hacer, se empezó a ejecutar desde arriba el código, el hechizo Lightning ya estaba de nuevo disponible (self.is_ready("lightning") = True). Por lo tanto el primer condicional de nuevo se cumplió. Y el personaje lanzó un rayo, no se protegió con el escudo.

La solución: los combos

La solución para evitar que pasen estas cosas es crear un bloque de programación cuya lógica diga "una vez empieza esta secuencia de hechizos, se empieza y se acaba".

Para llevar a cabo un combo, es imprescindible saber utilizar la memoria de nuestro personaje.

Podéis encontrar la documentación de la memoria a partir de la Misión 5 (Desertor)de la Campaña 2 (Decisión).

A partir del código anterior, vamos a crear el combo combo_heal (podemos ponerle el nombre que queramos):

combo = self.memory["combo"]

if combo is None and self.health < self.max_health * 0.45:
    combo = "combo_heal"
    self.memory["combo"] = combo
    self.memory["combo_step"] = "cast_heal"
    
if combo is None:
    if self.is_ready("lightning") and self.cost_of("lightning") < self.mana:
        return ["cast", "lightning", target]
    if self.is_ready("corruption") and self.cost_of("corruption") < self.mana:
        return ["cast", "corruption", self]
elif combo == "combo_heal":
    combo_step = self.memory["combo_step"]

    if combo_step == "cast_heal":
        if self.is_ready("heal") and self.cost_of("heal") < self.mana:
            self.memory["combo_step"] = "cast_shield"
            return ["cast", "heal", self]
    elif combo_step == "cast_shield":
        if self.is_ready("shield") and self.cost_of("shield") < self.mana:
            self.memory["combo"] = None
            self.memory["combo_step"] = None
            return ["cast", "shield", self]
        

Hay muchas formas de trabajar con combos, y este código podría ser más o menos complicado, pero para entender el mecanismo de los cambos, será suficiente.

¡Vamos a ver qué hemos hecho!

Utilizando la memoria de nuestro personake

En primer lugar, hemos utilizado la memoria de nuestro personaje, el diccionario self.memory[], para permitir que se acuerde ciclo a ciclo de ejecución de la partida si tiene o no un combo activado, self.memory["combo"].

Más tarde, también utilizamos la memoria para recordar en qué paso del combo estamos, self.memory["combo_step"].

Si no hay combo, atacamos, si estamos bajos de vida, activamos el combo

Como podéis ver en el ejemplo, si nuestro personaje detecta que no hay combo activado, if combo is None, pero que tiene por dejado del 45% su vida, and self.health < self.max_health * 0.45, entonces introducimos en su memoria que en los próximos ciclos de ejecución de la partida queremos llevar a cabo el combo combo_heal, y además establecemos el primer paso que queremos hacer, lanzar el hechizo Heal, self.memory["combo_step"] = "cast_heal":

combo = "combo_heal"
self.memory["combo"] = combo
self.memory["combo_step"] = "cast_heal"

Si hay combo, de principio a fin

Una vez activado el combo de curación, si miramos el código, veremos que en primer lugar nuestro personaje recurrirá a si memoria para saber en qué paso del combo está, combo_step = self.memory["combo_step"].

El resto es sencillo. Cada vez que lleva a cabo un paso del combo, establece el siguiente que quiere hacer, self.memory["combo_step"] = "cast_shield", hasta que finalmente el último paso del combo, en este caso cast_shield, reinicia la memoria del personaje:

self.memory["combo"] = None
self.memory["combo_step"] = None

No ha importado si el hechizo Lightning o el hechizo Corruption estaban disponibles: nuestro personaje quería curarse y protegerse, de principio a fin, sin interrupciones.

Y lo ha logrado, ¡gracias a un combo!