¡Hola, Comunidad Coded!

Con la ayuda de nuestros personajes LeGo y Roco, os vamos a enseñar una sencilla estrategia de equipo: el healer estratega.

¿A quién va dirigido este artículo?

A todas aquellas personas que quieran competir en los torneos de CodedArena, o en los torneos de sus centros escolares.

Sin embargo, está dirigido especialmente a estudiantes y docentes que hayan empezado hace poco en el mundo de la programación en Python con Codedarena.

Aviso

El código contenido en este artículo se centra en explicar de la forma más clara posible una estrategia a nuevos estudiantes, más que en la optimización, la simplificación o las buenas o malas prácticas al programar en Python.

Por ejemplo, hay un conjunto de condicionales que se podrían agrupar en un único condicional. El código

    if self.team_id != mob.team_id and target is None:
        target = mob
    elif self.team_id != mob.team_id and mob.health < target.health:
        target = mob

podría escribirse como

    if self.team_id != mob.team_id and (target is None or mob.health < target.health):
        target = mob

Sin embargo, al tratar de explicar la estrategia a estudiantes que han empezado hace poco con la programación, es más sencilla la primera forma que la segunda.

El equipo

Este artículo contempla los códigos y estrategias para un equipo de 2 personajes. En el caso que el equipo sea de 4 personajes, la condición para seleccionar a nuestro aliado podría ser más completa, y por ejemplo buscar al aliado que tiene menos vida para curarle.

No obstante, estos casos quedan fuera del objetivo de este artículo.

El código y el vídeo

Podéis encontrar el código completo de los dos personajes en la parte final de este artículo.

Recomendamos la lectura del artículo para entender el porqué de los diferentes bloques de código.

En las diferentes secciones nos centraremos en explicar el bloque o bloques de código más importantes. En el código completo podréis encontrar inicializaciones de variables y conceptos más secundarios.

Además, encontraréis el enlace a un vídeo con una prueba de esta estrategia.

Estrategia

Uno de los personajes del equipo analizará y determinará los rivales contra los que luchar (el estratega), le comunicará el objetivo actual al resto del equipo, y se dedicará a curar a los aliados a distancia (el healer).

Mientras tanto, el otro personaje se centrará en luchar cuerpo a cuerpo con el rival objetivo que le indique el estratega (el melee).

El estratega

Vamos a hacer que Roco adopte el rol de estratega del equipo: aquel que analizará el estado de la partida, y enviará órdenes al resto.

Seleccionar al rival adecuado

Para este artículo, la estrategia de siguiente objetivo contra el que luchará el equipo será la de aquel rival que tenga la vida más baja.

for mob in mobs:
    if self.team_id != mob.team_id and target is None:
        target = mob
    elif self.team_id != mob.team_id and mob.health < target.health:
        target = mob

Almacenaremos en la variable target al primer personaje que encontremos en la lista mobs que no pertenezca al mismo equipo que el nuestro (self.team_id != mob.team_id and target is None), o también en el caso que el rival que encontremos tiene la vida más baja que el anterior que hayamos encontrado (self.team_id != mob.team_id and mob.health < target.health).

Enviar un mensaje al resto del equipo

Mediante el envío de mensajes le indicaremos al resto del equipo en qué rival deben finalizar sus hechizos.

if target.name != self.memory["current_target_name"]:
    self.memory["current_target_name"] = target.name
    self.send_message(target.name)
    return "New target: " + target.name

Cuando nuestro healer estratega haya encontrado un nuevo rival con menos vida que el objetivo actual:

  • Se guardará en su memoria el nombre del nuevo rival objetivo del equipo (self.memory["current_target_name"] = target.name)
  • Le enviará un nuevo mensaje al equipo con el nombre del nuevo objetivo(self.send_message(target.name))
  • Dirá una frase para que veamos que el equipo tiene un nuevo rival objetivo.

Un caso particular es la primera vez que se selecciona un rival. En ese caso, como la memoria de nuestro estratega está vacía (no había seleccionado ningún objetivo), seguro que el nombre del nuevo objetivo es diferente a la memoria vacía (target.name != self.memory["current_target_name"]).

El healer

Tal y como hemos comentado, el estratega será además el encargado de mantener a los aliados con vida.

Encontrando a nuestro aliado

Para la tarea de mantener con vida a nuestro aliado, en primer lugar debemos seleccionarlo dentro de la lista mobs.

for mob in mobs:
    if self.team_id == mob.team_id and self.name != mob.name:
        friend = mob
        break

Aliado será aquel que esté en nuestro equipo (self.team_id == mob.team_id), y que además no sea nuestro propio personaje, es decir, que no tenga nuestro mismo nombre (self.name != mob.name).

Manteniendo la distancia

Una vez encontrado a nuestro aliado, mantendremos la distancia con respecto a este mediante el siguiente código.

    if self.distance_to(friend) > self.max_distance_of("heal"):
        if self.x - self.speed > target.x:
            move_x = "left"
        elif self.x + self.speed < target.x:
            move_x = "right"

        if self.y - self.speed > self.y:
            move_y = "up"
        elif self.y + self.speed < target.y:
            move_y = "down"
        
        return ["move", move_x, move_y]

Estrategia de curación

Una vez estemos a distancia de curación de nuestro aliado, tenemos que decidir si damos prioridad a la supervivencia de nuestro personaje, o a la del aliado.

En este artículo, daremos prioridad a nuestro personaje, ya que es el único healer del equipo.

Estrategia de defensa

Si el healer detecta que su vida empieza a bajar del 85%, se protegerá en primer lugar con un hechizo Shield, y después iniciará la curación mientras el escudo absorbe daño.

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

Cuando a los aliados

Si, por el contrario, el healer se encuentra por encima del 85% de vida, se centrará en la curación del aliado.

Si detecta que la vida del aliado baja del 50%, empezará la estrategia de curación con el hechizo Heal.

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

El melee

Una vez visto el código del estratega healer, vamos a ver qué debe hacer el código del personaje que luchará cuerpo a cuerpo, LeGo.

¿Ha llegado algún mensaje nuevo?

Lo primero que debemos hacer es analizar los mensajes que nos envíe el estratega. Además, con la ayuda de la memoria del personaje, acordarnos de qué mensajes ya hemos procesado.

for message in self.messages:
    if message not in self.memory["messages_processed"] and message["who"] == "Roco":
        self.memory["target_name"] = message["what"]
        self.memory["messages_processed"].append(message)

Si nuestro personaje detecta que hay llegado un nuevo mensaje (message not in self.memory["messages_processed"]), y que nos lo ha enviado nuestro estratega Roco (message["who"] == "Roco"):

  • Nos guardamos en la memoria del personaje el nombre del objetivo, para poder seleccionarlo en cada tick del juego (self.memory["target_name"] = message["what"]).
  • Guardamos el mensaje que acabamos de procesar en la memoria (self.memory["messages_processed"].append(message)), para no volver a procesarlo (message not in self.memory["messages_processed"]).

¿Tenemos un rival objetivo?

Sí, tenemos un objetivo

Si tenemos en la memoria de nuestro personaje el nombre de un rival, lo buscamos en la lista mobs.

if self.memory["target_name"] is not None:
    # Bucle para localizar al adversario
    for mob in mobs:
        if mob.name == self.memory["target_name"]:
            target = mob
            break

Una vez localizado, si estamos lejos (p.e. más allá de la distancia de invocación del hechizo Lightning), nos invocamos un hechizo de velocidad, y nos acercamos rápidamente.

La comprobación de la distancia del hechizo Lightning la hacemos para evitar que nuestro personaje esté constantemente lanzándose el hechizo de velocidad aunque esté ya muy cerca del objetivo (lo cual sería una pérdida de tiempo y de maná).

    if self.is_ready("speed") and \
        self.mana > self.cost_of("speed") and \
        self.distance_to(target) > self.max_distance_of("Lightning"):
        return ["cast", "speed", self]

    if self.distance_to(target) > self.max_distance_of("slam"):
        if self.x - self.speed > target.x:
            move_x = "left"
        elif self.x + self.speed < target.x:
            move_x = "right"

        if self.y - self.speed > target.y:
            move_y = "up"
        elif self.y + self.speed < target.y:
            move_y = "down"
        
        return ["move", move_x, move_y]

Si en cambio estamos ya cerca del rival objetivo, lanzamos nuestra estrategia ofensiva. En este caso, invocar los hechizos cuerpo a cuerpo Slam y Swipe, dando prioridad al primero sobre el segundo.

    else:
        if self.is_ready("slam") and self.mana > self.cost_of("slam"):
            return ["cast", "slam", target]

        if self.is_ready("swipe") and self.mana > self.cost_of("swipe"):
            return ["cast", "swipe", target]

No, el estratega todavía no nos ha comunicado el objetivo

Esperamos :)

else:
    return "Waiting for a target!"

En este caso, lo suyo sería crear una estrategia que le ayude a nuestro personaje a aprovechar el tiempo, como por ejemplo ponerse un escudo mágico, curarse, etc.

El objetivo de este artículo no es establecer esta estrategia (ya la abordaremos en próximos artículos), así que esperaremos pacientemente a que nuestro aliado estratega decide qué rival objetivo es el adecuado.

Los códigos completos

Roco, el estratega healer

# Inicializamos la memoria del personaje para que recuerde el nombre del rival objetivo del equipo
if "current_target_name" not in self.memory:
    self.memory["current_target_name"] = None

# Variables que se reinician en cada tick del videojuego
target = None
friend = None
move_x = "none"
move_y = "none"

# Buscamos a nuestro aliado (solo válido para equipo de dos personajes)
for mob in mobs:
    if self.team_id == mob.team_id and self.name != mob.name:
        friend = mob
        break

# Buscamos el rival que tiene menor vida en la lista mobs
for mob in mobs:
    if self.team_id != mob.team_id and target is None:
        target = mob
    elif self.team_id != mob.team_id and mob.health < target.health:
        target = mob

# Si el rival que hemos encontrado con menos vida no es el objetivo actual, cambiamos de objetivo
# y se lo notificamos al equipo mediante el envío de un mensaje
if target.name != self.memory["current_target_name"]:
    self.memory["current_target_name"] = target.name
    self.send_message(target.name)
    return "New target: " + target.name

if friend is not None and target is not None:
    # Mantenemos la distancia de curación con respecto a nuestro aliado
    if self.distance_to(friend) > self.max_distance_of("heal"):
        if self.x - self.speed > target.x:
            move_x = "left"
        elif self.x + self.speed < target.x:
            move_x = "right"

        if self.y - self.speed > self.y:
            move_y = "up"
        elif self.y + self.speed < target.y:
            move_y = "down"
        
        return ["move", move_x, move_y]
    else:
        # Damos prioridad a la curación de nuestro personaje, si su vida baja del 85%, 
        # protegiéndolo con Shield (prioritario) y curándolo con Heal
        if self.health <= self.max_health * 0.85:
            if self.is_ready("shield") and self.cost_of("shield") < self.mana:
                return ["cast", "shield", self]
            if self.is_ready("heal") and self.cost_of("heal") < self.mana:
                return ["cast", "heal", self]

        # Si nuestro personaje está por encima del 75% de vida, nos centramos en curar a nuestro aliado
        # si su vida baja del 50%
        if friend.health <= friend.max_health * 0.5 and self.is_ready("heal") and self.cost_of("heal") < self.mana:
            return ["cast", "heal", friend]
elif friend is None:
    # Si no encontramos a nuestro aliado, es que estamos en un problema. 
    # ¿Quizá deberíamos tener una estrategia ofensiva como alternativa?
    return "Can I say 'Ouch'?"

LeGo, el mago melee

# Inicializamos la memoria del personaje para que recuerde el nombre del rival objetivo del equipo
if "target_name" not in self.memory:
    self.memory["target_name"] = None 

# Inicializamos la memoria del personaje para que recuerde los mensajes ya procesados y que no queremos
# volver a tratar
if "messages_processed" not in self.memory:
    self.memory["messages_processed"] = []

# Variables que se reinician en cada tick del videojuego
target = None
move_x = "none"
move_y = "none"

# Por cada mensaje recibido (si los hay), tratamos aquellos no procesados y que nos haya
# enviado nuestro estratega Roco
for message in self.messages:
    if message not in self.memory["messages_processed"] and message["who"] == "Roco":
        self.memory["target_name"] = message["what"]
        self.memory["messages_processed"].append(message)

if self.memory["target_name"] is not None:
    # Buscamos al rival objetivo que nos ha indicado nuestro estratega
    for mob in mobs:
        if mob.name == self.memory["target_name"]:
            target = mob
            break

    if target is None:
        return "No target found!"

    # Nos acercamos lo más rápidamente posible (con el hechizo de velocidad), solo si
    # estamos muy lejos
    if self.is_ready("speed") and \
        self.mana > self.cost_of("speed") and \
        self.distance_to(target) > self.max_distance_of("Lightning"):
        return ["cast", "speed", self]

    if self.distance_to(target) > self.max_distance_of("slam"):
        if self.x - self.speed > target.x:
            move_x = "left"
        elif self.x + self.speed < target.x:
            move_x = "right"

        if self.y - self.speed > target.y:
            move_y = "up"
        elif self.y + self.speed < target.y:
            move_y = "down"
        
        return ["move", move_x, move_y]
    else:
        if self.is_ready("slam") and self.mana > self.cost_of("slam"):
            return ["cast", "slam", target]

        if self.is_ready("swipe") and self.mana > self.cost_of("swipe"):
            return ["cast", "swipe", target]
else:
    # Esperamos pacientemente la llegada de un mensaje con el rival objetivo
    return "Waiting for a target!"

Si os ha gustado este artículo y queréis saber qué es CodedArena, no dudéis en poneros en contacto con nosotros.

También podéis seguirnos en nuestras redes sociales:

https://twitter.com/codedarena
https://instagram.com/codedarena
https://www.youtube.com/channel/CodedArena
https://facebook.com/codedarena