Partie 5 - Ajouter des ennemis et leur donner des coups de pied (sans faire mal)


Qu’est-ce qu’un donjon sans monstre à cogner ? Ce chapitre se concentrera sur la disposition des ennemis à travers le donjon et les configurer pour qu’on puisse les attaquer (les vrais combats seront abordés plus tard). Pour commencer nous aurons besoin d’une fonction pour positionner les ennemis dans le donjon. Appelons la place_entities et ajoutons la à la classe GameMap.

    def create_v_tunnel(self, y1, y2, x):
        ...

+   def place_entities(self, room, entities, max_monsters_per_room):
+       # Get a random number of monsters
+       number_of_monsters = randint(0, max_monsters_per_room)
+
+       for i in range(number_of_monsters):
+           # Choose a random location in the room
+           x = randint(room.x1 + 1, room.x2 - 1)
+           y = randint(room.y1 + 1, room.y2 - 1)
+
+           if not any([entity for entity in entities if entity.x == x and entity.y == y]):
+               if randint(0, 100) < 80:
+                   monster = Entity(x, y, 'o', libtcod.desaturated_green)
+               else:
+                   monster = Entity(x, y, 'T', libtcod.darker_green)
+
+               entities.append(monster)

    def is_blocked(self, x, y):
        ...
    def create_v_tunnel(self, y1, y2, x):
        ...

    def place_entities(self, room, entities, max_monsters_per_room):
        # Get a random number of monsters
        number_of_monsters = randint(0, max_monsters_per_room)

        for i in range(number_of_monsters):
            # Choose a random location in the room
            x = randint(room.x1 + 1, room.x2 - 1)
            y = randint(room.y1 + 1, room.y2 - 1)

            if not any([entity for entity in entities if entity.x == x and entity.y == y]):
                if randint(0, 100) < 80:
                    monster = Entity(x, y, 'o', libtcod.desaturated_green)
                else:
                    monster = Entity(x, y, 'T', libtcod.darker_green)

                entities.append(monster)

    def is_blocked(self, x, y):
        ...

Dans cette fonction on choisit un nombre aléatoire d’ennemis à disposer, entre 0 et le maximum qu’on choisit. Ensuite on tire un x et y aléatoire et, si aucun ennemi n’est déjà à cet endroit, on y place un monstre. Il y à 80% de chance que ce soit un Orc et 20% de chance que ce soit un Troll.

Nous aurons besoin d’importer à la fois libtcod et la classe Entity.

+import tcod as libtcod
from random import randint

+from entity import Entity
from map_objects.rectangle import Rect
from map_objects.tile import Tile
import tcod as libtcod
from random import randint

from entity import Entity
from map_objects.rectangle import Rect
from map_objects.tile import Tile

Maintenant modifions notre fonction make_map pour y inclure la fonction place_entities.

                        ...
                        self.create_h_tunnel(prev_x, new_x, new_y)

+               self.place_entities(new_room, entities, max_monsters_per_room)

                rooms.append(new_room)
                ...
                        ...
                        self.create_h_tunnel(prev_x, new_x, new_y)

                self.place_entities(new_room, entities, max_monsters_per_room)

                rooms.append(new_room)
                ...

Parce que nous avons maintenant besoin des variables entities et max_monsters_per_room, nous devrions modifier la définition de la fonction make_map pour les y inclure.

-   def make_map(self, max_rooms, room_min_size, room_max_size, map_width, map_height, player):
+   def make_map(self, max_rooms, room_min_size, room_max_size, map_width, map_height, player, entities,
+                max_monsters_per_room):
    def make_map(self, max_rooms, room_min_size, room_max_size, map_width, map_height, player):
    def make_map(self, max_rooms, room_min_size, room_max_size, map_width, map_height, player, entities,
                 max_monsters_per_room):

Tout est bon ici, maintenant nous devons modifier engine.py pour tenir compte de cette nouvelle fonction make_map. Aussi nous aurons besoin de créer la variable max_room_per_monsters avant d’appeler la fonction. Enfin nous changerons notre liste entities pour qu’elle ne contienne que le joueur, retirant complètement notre NPC d’exemple utilisé plus tôt.

    ...
    fov_radius = 10

+   max_monsters_per_room = 3

    colors = {
        'dark_wall': libtcod.Color(0, 0, 100),
        'dark_ground': libtcod.Color(50, 50, 150),
        'light_wall': libtcod.Color(130, 110, 50),
        'light_ground': libtcod.Color(200, 180, 50)
    }

-   player = Entity(int(screen_width / 2), int(screen_height / 2), '@', libtcod.white)
-   npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), '@', libtcod.yellow)
-   entities = [npc, player]
+   player = Entity(0, 0, '@', libtcod.white)
+   entities = [player]

    libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD)

    libtcod.console_init_root(screen_width, screen_height, 'libtcod tutorial revised', False)

    con = libtcod.console_new(screen_width, screen_height)

    game_map = GameMap(map_width, map_height)
-   game_map.make_map(max_rooms, room_min_size, room_max_size, map_width, map_height, player)
+   game_map.make_map(max_rooms, room_min_size, room_max_size, map_width, map_height, player, entities, max_monsters_per_room)

    fov_recompute = True
    ...
    ...
    fov_radius = 10

    max_monsters_per_room = 3

    colors = {
        'dark_wall': libtcod.Color(0, 0, 100),
        'dark_ground': libtcod.Color(50, 50, 150),
        'light_wall': libtcod.Color(130, 110, 50),
        'light_ground': libtcod.Color(200, 180, 50)
    }

    player = Entity(int(screen_width / 2), int(screen_height / 2), '@', libtcod.white)
    npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), '@', libtcod.yellow)
    entities = [npc, player]
    player = Entity(0, 0, '@', libtcod.white)
    entities = [player]

    libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD)

    libtcod.console_init_root(screen_width, screen_height, 'libtcod tutorial revised', False)

    con = libtcod.console_new(screen_width, screen_height)

    game_map = GameMap(map_width, map_height)
    game_map.make_map(max_rooms, room_min_size, room_max_size, map_width, map_height, player)
    game_map.make_map(max_rooms, room_min_size, room_max_size, map_width, map_height, player, entities, max_monsters_per_room)

    fov_recompute = True
    ...

Lancez le projet maintenant et vous devriez voir quelques orcs et trolls peupler notre donjon !

Un problème évident avec les monstres qu’on vient d’inventer (au dela du fait qu’ils sont totalement inanimés…) est que le joueur peut les traverser. À moins que vous n’ayez l’intention de créer un jeu sur un fantôme qui traverse des monstre pour en prendre possession (ça ne semble pas être une si mauvaise idée !), ce n’est pas ce que nous souhaitons. Si le joueur “se déplace dans” un ennemi, on devrait l’attaquer !

On pourrait croire qu’il suffit de vérifier si on se déplacer dans une Entity et l’attaquer si c’est le cas mais nous aurons besoin que certaines entités ne bloquent pas le mouvement. Pourquoi ? Parce que nous utiliserons cette classe Entity pour représenter les objets et nous voudrons marcher dessus pour les ramasser. Aussi il semble qu’il nous faille un attribut de classe nous disant si l’entité bloque le mouvement ou non.

Modifions la classe Entity pour y inclure la variable “block”. Profitons de cette occasion pour passer à la classe le nom (“name”) pour l’entité. Nous en aurons besoin d’ici peu.

class Entity:
-   def __init__(self, x, y, char, color):
+   def __init__(self, x, y, char, color, name, blocks=False):
        self.x = x
        self.y = y
        self.char = char
        self.color = color
+       self.name = name
+       self.blocks = blocks

    def move(self, dx, dy):
    ...
class Entity:
    def __init__(self, x, y, char, color):
    def __init__(self, x, y, char, color, name, blocks=False):
        self.x = x
        self.y = y
        self.char = char
        self.color = color
        self.name = name
        self.blocks = blocks

    def move(self, dx, dy):
    ...

Remarquons que “blocks” est optionnel. Si nous ne la déclarons pas à l’initialisation, elle sera False par défaut.

Revenons à game_map.py et modifions la méthode place_entities où nous déclarons nos monstres.

            if randint(0, 100) < 80:
-               monster = Entity(x, y, 'o', libtcod.desaturated_green)
+               monster = Entity(x, y, 'o', libtcod.desaturated_green, 'Orc', blocks=True)
            else:
-               monster = Entity(x, y, 'T', libtcod.darker_green)
+               monster = Entity(x, y, 'T', libtcod.darker_green, 'Troll', blocks=True)
            if randint(0, 100) < 80:
                monster = Entity(x, y, 'o', libtcod.desaturated_green)
                monster = Entity(x, y, 'o', libtcod.desaturated_green, 'Orc', blocks=True)
            else:
                monster = Entity(x, y, 'T', libtcod.darker_green)
                monster = Entity(x, y, 'T', libtcod.darker_green, 'Troll', blocks=True)

Nous devons aussi mettre à jour l’initialisation du joueur dans engine.py :

-   player = Entity(0, 0, '@', libtcod.white)
+   player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True)
    player = Entity(0, 0, '@', libtcod.white)
    player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True)

Avec nos nouveaux attributs nous devons nous assurer qu’une entité ne bloque pas le chemin quand nous essayons de nous déplacer sur une tuile. Cela nous aiderait d’avoir une fonction qui renvoie l’entité bloquante en lui donnant la liste des entités et les coordonnées x et y. Nous l’ajouterons à entity.py mais pas à la classe Entity elle même. La raison est que c’est une fonction en rapport avec les entités mais pas à une entité en particulier aussi elle n’appartient pas à la classe.

Ajoutez la fonction à entity.py comme ceci :

class Entity:
    ...


+def get_blocking_entities_at_location(entities, destination_x, destination_y):
+   for entity in entities:
+       if entity.blocks and entity.x == destination_x and entity.y == destination_y:
+           return entity
+
+   return None
class Entity:
    ...


def get_blocking_entities_at_location(entities, destination_x, destination_y):
    for entity in entities:
        if entity.blocks and entity.x == destination_x and entity.y == destination_y:
            return entity

    return None

La fonction boucle sur les entités et, si l’une est bloquante et placée aux x et y indiqués, on la renvoie. Si aucune ne correspond on renvoie “None”. Remarquez que la fonction suppose qu’une seule entité est placée à cette position. Cela ne devrait pas poser de problème car nous nous assurerons que deux entités ne peuvent se déplacer sur une même tuile.

Ceci étant fait, revenons à notre fonction de déplacement. Modifiez le code qui déplace le joueur dans engine.py comme ceci :

        if move:
            dx, dy = move

-           player.move(dx, dy)
-
-           fov_recompute = True

+           destination_x = player.x + dx
+           destination_y = player.y + dy

-           if not game_map.is_blocked(player.x + dx, player.y + dy):
+           if not game_map.is_blocked(destination_x, destination_y):
+               target = get_blocking_entities_at_location(entities, destination_x, destination_y)
+
+               if target:
+                   print('You kick the ' + target.name + ' in the shins, much to its annoyance!')
+               else:
+                   player.move(dx, dy)
+
+                   fov_recompute = True
        if move:
            dx, dy = move
            destination_x = player.x + dx
            destination_y = player.y + dy

            if not game_map.is_blocked(player.x + dx, player.y + dy):
            if not game_map.is_blocked(destination_x, destination_y):
                target = get_blocking_entities_at_location(entities, destination_x, destination_y)

                if target:
                    print('You kick the ' + target.name + ' in the shins, much to its annoyance!')
                else:
                    player.move(dx, dy)

                    fov_recompute = True

Assurez vous d’importer la fonction get_blocking_entities_at_location en haut de engine.py

-   from entity import Entity
+   from entity import Entity, get_blocking_entities_at_location
from entity import Entity, get_blocking_entities_at_location

Maintenant le joueur est bloqué quand il essaye de traverser une autre entité. Nous affichons un texte humoristique (hey, je trouve ça drôle !) pour l’instant. Nous implémenterons un vrai combat dans le chapitre suivant.

Notre joueur ne devrait pouvoir se déplacer que durant son tour et ce principe s’applique aux monstres. Nous aurons besoin d’une variable pour savoir à qui est le tour. Nous pourrions conserver une chaîne dans cette variable comme ‘players_turn’ ou ‘enemy_turn’ mais c’est succeptible de créer des erreurs. Si vous faîtes une typo en écrivant ces chaîne, vous allez créer des bugs. N’oublions pas que le nombre d’états du jeu va augmenter et nous aurons besoin d’une meilleure manière de les enregistrer.

Nous allons enregistrer ces états avec un Enum. Un “Enum” est un ensemble de valeurs qui ne changent pas et qu’on peut énumérer, parfait pour les états du jeu. Créer un nouveau fichier appelé game_states.py et ajoutez-y la classe suivante :

from enum import Enum


class GameStates(Enum):
    PLAYERS_TURN = 1
    ENEMY_TURN = 2

Cela rendra nos changements d’états du jeu plus faciles à gérer, en particulier quand nous en aurons plus de deux.

* Remarque : les nombres associés n’ont pas de sens précis. En fait, si vous Python 3.6 ou une version ultérieure, vous pouvez utiliser ‘auto’ pour incrémenter un nombre automatiquement. Vérifiez si c’est possible pour vous.

Mettons ces nouveaux enum GameStates en action. Commencez par les importer en haut.

...
from fov_functions import initialize_fov, recompute_fov
+from game_states import GameStates
from input_handlers import handle_keys
...
...
from fov_functions import initialize_fov, recompute_fov
from game_states import GameStates
from input_handlers import handle_keys
...

Enfin, créez une variable appelée game_state sera d’abord réglée sur le tour du joueur.

    ...
    mouse = libtcod.Mouse()

+   game_state = GameStates.PLAYERS_TURN

    while not libtcod.console_is_window_closed():
    ...
    ...
    mouse = libtcod.Mouse()

    game_state = GameStates.PLAYERS_TURN

    while not libtcod.console_is_window_closed():
    ...

Selon que ce soit le tour du joueur ou non, nous voulons contrôler le mouvement du joueur. Le joueur ne peut se déplacer que durant son tour aussi modifions notre section if move: pour en tenir compte. Une fois que notre joueur aura réussi à se déplacer, nous allons passer l’état à ENEMY_TURN.

-       if move:
+       if move and game_state == GameStates.PLAYERS_TURN:
            dx, dy = move
            destination_x = player.x + dx
            destination_y = player.y + dy

            if not game_map.is_blocked(destination_x, destination_y):
                target = get_blocking_entities_at_location(entities, destination_x, destination_y)

                if target:
                    print('You kick the ' + target.name + ' in the shins, much to its annoyance!')
                else:
                    player.move(dx, dy)

                    fov_recompute = True

+               game_state = GameStates.ENEMY_TURN
        if move:
        if move and game_state == GameStates.PLAYERS_TURN:
            dx, dy = move
            destination_x = player.x + dx
            destination_y = player.y + dy

            if not game_map.is_blocked(destination_x, destination_y):
                target = get_blocking_entities_at_location(entities, destination_x, destination_y)

                if target:
                    print('You kick the ' + target.name + ' in the shins, much to its annoyance!')
                else:
                    player.move(dx, dy)

                    fov_recompute = True

                game_state = GameStates.ENEMY_TURN

Si vous lancez le projet maintenant, votre joueur sera capable de se déplacer une fois… et sera bloqué pour toujours. C’est parce que nous devons implémenter les mouvements des ennemis et rendre le game_state au joueur ensuite. Remarquez que vous pouvez quitter le jeu ou le passer en plein écran parce que nous n’empéchons pas le joueur d’accomplir ces choses quand ça n’est pas son tour.

        ...
        if fullscreen:
            libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen())

+       if game_state == GameStates.ENEMY_TURN:
+           for entity in entities:
+               if entity != player:
+                   print('The ' + entity.name + ' ponders the meaning of its existence.')
+
+           game_state = GameStates.PLAYERS_TURN
        ...
        if fullscreen:
            libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen())

        if game_state == GameStates.ENEMY_TURN:
            for entity in entities:
                if entity != player:
                    print('The ' + entity.name + ' ponders the meaning of its existence.')

            game_state = GameStates.PLAYERS_TURN

C’est assez simple. Supposons que c’est le tour de l’ennemi, nous parcourons chaque entité, à l’exception du joueur, et nous leur donnons le tour. Pour l’instant nous n’avons pas d’AI pour nos ennemis donc ils restent immobiles à contempler leurs vies. Dans le prochain chapitre nous leur donnerons un comportement plus intéressant mais pour l’instant cela servira d’exemple.

Si vous voulez voir le code actuel entièrement, cliquez ici.

Cliquez ici pour vous rendre à la partie suivante de ce tutoriel.