Skip to main content

Reinforced Frog

Github

Jumper Frog (Frogger Game) in python with AI reinforcement 🐸

README-1666710399073.gif armored_frog.png

Présentation du jeu et Objectif

L'objectif principal est de faire apprendre par renforcement un agent sur le jeu Frogger.

Contexte

Ce projet a été réalisé dans le cadre du cours d'apprentissage par renforcement. Il a été réalisé par 3 étudiants en
 5ᵉ année d'architecture logicielle.

Jeu original

Règle du jeu
Image du jeu original
Frogger est un jeu d'arcade classique. Le but du jeu est de diriger des grenouilles jusqu'à leurs maisons. Pour cela, le joueur doit d'abord traverser une route en évitant des voitures qui roulent à différentes vitesses, puis une rivière aux courants changeants et enfin, à nouveaux, une route. La grenouille meurt si elle touche une voiture ou si elle tombe dans la rivière. Frogger_game_arcade.png

Objectif

L'objectif est de faire apprendre à un agent à traverser la route et la rivière en évitant les voitures et l'eau.
Pour cela, nous allons utiliser l'algorithme Q-Learning. L'agent va apprendre à traverser la route et la rivière en
 apprenant à associer une action à un état. L'agent va donc découvrir comment associer une action à un état.

Pour cela, nous allons également devoir développer le jeu Frogger en utilisant la
 librairie arcade. Le seul langage utilisé est le Python, nous n'utilisons pas de librairie externe mis à part arcade et quelques librairies utilitaires.

Installation

Prérequis

    Python 3.10 minimum PIP3

    Installation des dépendances

    Après avoir cloné le projet, il faut installer les dépendances avec la commande suivante :

    pip3 install -r requirements.txt

    Utilisation

    Environnement

    Avant de lancer le jeu, il faut créer le fichier .env à la racine du projet. Ce fichier contient les variables d'environnement nécessaire au bon fonctionnement du jeu. Vous pouvez vous baser sur le fichier .env.example pour créer le fichier .env.

    Variable
    Description
    Valeur conseillée
    AGENT_COUNT Nombre d'agents en simultané sur la carte 1-10 AGENT_DEBUG Afficher les informations debugs en console des agents (WIN/LOOSE) false
    ARCADE_INSIGHTS Afficher les informations de l'agent sur le jeu arcade true
    AGENT_GAMMA Taux de prise en compte de l'état futur 0.1
    AGENT_LEARNING_FILE Emplacement du fichier qtable qtable/nom-du-fichier.xz AGENT_LEARNING_RATE Taux d'apprentissage de l'agent 0.6
    AGENT_VISIBLE_COLS_ARROUND Nombre de colonnes visible autour de l'agent dans son environnement 4-6
    AGENT_VISIBLE_LINES_ABOVE Nombre de lignes visible devant l'agent dans son environnement 1-2
    EXPLORE_RATE Taux d'exploration sur les actions déterminés de l'agent 0.05-0.1
    EXPLORE_RATE_DECAY Taux de diminution du taux d'exploration 0.999
    GENERATE_HISTORY_GRAPH Générer le graphique de progression d'apprentissage en même temps que la sauvegarde de la Q-Table true
    HASH_QTABLE Hash des lignes de l'environnement (permet de diminuer un peu la taille en mémoire) false
    LEARNING_MODE Passer en mode apprentissage (console seulement) ou en mode graphique (arcade) true pour apprendre un peu, puis false
    LEARNING_TYPE Type d'apprentissage (QLEARNINGMQLEARNINGDQLEARNING) QLEARNING-MQLEARNING 
    LEARNING_TIME Temps de l'apprentissage en minute 45
    LEARNING_PRINT_STATS_EVERY Afficher en console les stats d'appretissage tous les x secondes 30-60
    LEARNING_SAVE_QTABLE_EVERY Fréquence de sauvegarde de la Q-Table tous les x secondes (opération lourde) 60-600
    QTABLE_HISTORY_FILE Emplacement des fichiers d'historique history/nom-du-fichier.history QTABLE_HISTORY_PACKETS Paquets pour l'historique = au nombre d'agents (1-10) WORLD_TYPE Type de monde ( 0 -> Route + Eau, 1 -> Route seulement, 2 -> Eau uniquement) 0

    Pour lancer le jeu, il faut lancer la commande suivante :

    python3 main.py

    Développement du jeu

    Présentation de la librairie arcade

    Arcade est une librairie Python permettant de créer des jeux vidéo. Elle est basée sur Pyglet et permet de créer des jeux vidéo 2D. Elle permet de créer des jeux vidéo en 2D avec des sprites, des animations, des sons, des effets de particules…,

    img.png

    Configuration des règles

    Afin de pouvoir modifier rapidement la configuration de notre jeu (difficulté, tokens, actions possibles…,), nous avons écrit toute la configuration dans le fichier config.py. Ce fichier est lu par le jeu.

    Tokens

    Ce fichier de configuration contient les différents tokens utilisés dans le jeu. Ces derniers permettent au jeu d'avoir une représentation textuelle de son environement, ce qui va grandement nous aider pour l'apprentissage de l'agent.

    CAR_TOKEN = 'C'
    TRUCK_TOKEN = 'Z'
    TURTLE_TOKEN = 'T'
    TURTLE_L_TOKEN = 'TL'
    TURTLE_XL_TOKEN = 'TXL'
    REVERSED_CAR_TOKEN = 'RC'
    REVERSED_TRUCK_TOKEN = 'RZ'
    REVERSED_TURTLE_TOKEN = 'RT'
    REVERSED_TURTLE_L_TOKEN = 'RTL'
    ...
    
    ACTION_UP = 'U'
    ACTION_DOWN = 'D'
    ACTION_LEFT = 'L'
    ACTION_RIGHT = 'R'
    ACTION_NONE = 'N'
    ...
    
    WATER_COMMONS_TOKENS = [TURTLE_TOKEN, TURTLE_L_TOKEN, TURTLE_XL_TOKEN, REVERSED_TURTLE_TOKEN, REVERSED_TURTLE_L_TOKEN,
                            ...]
    ...
    
    WIN_STATES = [EXIT_TOKEN]

    Arcade

    Ce fichier de configuration contient les différents paramètres de la librairie arcade. Ces derniers permettent de définir les sprites des différentes entités, ainsi que leur taille et le scaling.

    SCALE = 1
    SPRITE_SIZE = 64 * SCALE
    
    ...
    
    
    def get_sprite_resources(name: str, sprite_size: float = 0.5):
      return arcade.Sprite(f":resources:images/{name}.png", sprite_size * SCALE)
    
    
    def get_sprite_local(name: str, sprite_size: float = 0.5):
      return arcade.Sprite(f"assets/sprite/{name}.png", sprite_size * SCALE)
    
    
    ENTITIES: Dict[str, WorldEntity] = {
      CAR_TOKEN: WorldEntity(1, 1, CAR_TOKEN, get_sprite_local("car_1", 0.65)),
      ...
    }
    
    ...
    
    WORLD_WIDTH = 180
    WORLD_HEIGHT = 117
    WORLD_SCALING = 9

    Représentation du monde

    Le monde est représenté par une matrice de caractères. Chaque caractère représente une entité du monde. Les entités sont représentées par des tokens. Ces derniers sont définis dans le fichier de configuration.

    La classe permettant de représenter le monde est la classe World. Cette classe permet de

    Cette dernière permet de :

      Créer un monde, avec la bonne configuration Gérer la mise à jour (déplacement) des entités dans le monde Gérer les collisions entre les entités Gérer les mouvements, récompenses des joueurs

      Représentation du monde pour l'agent

      À chaque état, l'agent reçoit une représentation du monde sous forme de liste de chaîne caractères. Chaque élément représente une ligne visible (définit dans les variables d'environnement) du monde.

      Ainsi, l'agent ne voit pas toute la carte, mais tout au plus 2 ligne devant lui, 1 ligne derrière lui, et 4 colonnes sur les côtés.

      README-1667658404493.png

      World Entity

      Pour généraliser les différentes entités que nous traitons, nous avons la classe WorldEntity. Cette dernière permet de regrouper pour chaque entité :

        La taille de l'entité Le token de l'entité Le sprite de l'entité

        World Line

        La classe WorldLine permet de représenter une ligne du monde. Cette dernière permet de gérer les déplacements des entités sur chaque ligne, ainsi que leur fréquence d'apparition, vitesse…,

        Joueurs

        Le joueur est représenté par l'interface Player. Cette dernière permet de gérer le déplacement du joueur, de le gérer dans la classe principale Game. Cette interface nous permet de gérer plusieurs types de joueurs ( Humain, Agent).

        from typing import Tuple, List
        
        from arcade import Sprite
        
        from display.entity.world_entity import WorldEntity
        from game.world import World
        
        
        class Player:
          def init(self, world: World, intial_state: Tuple[int, int], _initial_environment: bytes):
            pass
        
          def best_move(self, environment: [str]) -> str:
            pass
        
          def step(self, action: str, reward: float, new_state: Tuple[int, int], _environment: List[str]):
            pass
        
          def save_score(self):
            pass
        
          def update_state(self, new_state, new_environment):
            pass
        
          @property
          def sprite(self) -> Sprite:
            pass
        
          @property
          def world_entity(self) -> WorldEntity:
            pass
        
          @property
          def is_human(self) -> bool:
            pass
        
          @property
          def score(self) -> int:
            pass
        
          @property
          def state(self) -> Tuple[int, int]:
            pass

        Joueur Humain

        Le joueur humain est représenté par la classe HumanPlayer. Cette dernière permet de se déplacer avec les touches directionnelles du clavier.

        Affichage graphique

        L'affichage graphique est entièrement géré par la classe WorldWindow. Cette dernière permet de gérer l'affichage du monde sur arcade. Elle permet également de gérer la vitesse de jeu, le nombre de frames par seconde, les statistiques affichés…,

        import arcade.color
        
        from ai.Model import Model
        from conf.config import *
        from game.game import Game
        
        
        class WorldWindow(arcade.Window):
          def __init__(self, game: Game, env, model: Model):
            super().__init__(
              int(game.world.width / WORLD_SCALING * SPRITE_SIZE),
              int(game.world.height / WORLD_SCALING * SPRITE_SIZE),
              'REINFORCED FROG',
              update_rate=1 / 60
            )
            self.__height = int(game.world.height / WORLD_SCALING * SPRITE_SIZE)
        
          # ...
        
          # ...
        
          def setup(self):
            self.setup_world_states()
            self.setup_players_states()
            self.setup_world_entities_state()
        
          def setup_world_entities_state(self):
            self.__entities_sprites = arcade.SpriteList()
            for state in self.__game.world.world_entities_states.keys():
              world_entity: WorldEntity = self.__game.world.get_world_entity(state)
              if world_entity is not None:
                sprite = self.__get_entity_sprite(state, world_entity)
                self.__entities_sprites.append(sprite)
        
          def setup_world_states(self):
            self.__world_sprites = arcade.SpriteList()
            for state in self.__game.world.world_states:
              world_entity: WorldEntity = self.__game.world.get_world_line_entity(state)
              if world_entity is not None:
                sprite = self.__get_environment_sprite(state, world_entity)
                self.__world_sprites.append(sprite)
        
          def setup_players_states(self):
            self.__players_sprites = arcade.SpriteList()
            for player in self.__game.players:
              sprite = player.sprite
              sprite.center_x, sprite.center_y = (
                self.__get_xy_state((player.state[0] + WORLD_SCALING // 2, player.state[1] + WORLD_SCALING // 2)))
              self.__players_sprites.append(sprite)
        
          def __draw_debug(self):
        
          # ...
        
          def on_draw(self):
            arcade.start_render()
            self.__world_sprites.draw()
            self.__entities_sprites.draw()
            self.__players_sprites.draw()
            if self.__debug == 1:
              self.__draw_debug()
            elif self.__debug == 2:
              self.__draw_collisions_debug()
            if self.__env['ARCADE_INSIGHTS']:
              self.__draw_model_insights()
        
          def __draw_model_insights(self):
        
          # ...
        
          def on_update(self, delta_time: float):
            self.__game.step()
            self.setup_players_states()
            self.__players_sprites.update()
            self.setup_world_entities_state()
            self.__entities_sprites.update()
        
          # ...

        Développement de l'IA

        Pour l'IA, nous avons utilisé 3 méthodes principales :

          Q-Learning Multi-Q-Learning Deep Q-Learning

          Q-Learning

          Le Q-Learning est une méthode d'apprentissage par renforcement. Cette méthode permet de déterminer la meilleure action à effectuer dans un état donné. Pour cela, elle utilise une fonction de valeur Q qui permet de déterminer la valeur d'une action dans un état donné. Cette fonction est mise à jour à chaque étape de l'apprentissage.

          Implémentation

          L'implémentation du Q-Learning est géré par la classe QLearning. Cette dernière permet de gérer la table de Q-Learning, de mettre à jour les valeurs de la table, de récupérer la meilleure action à effectuer, de sauvegarder la table de Q-Learning, ...

          Cette classe permet également (comme les autres méthodes d'apprentissage) de gérer l'exploration et l'exploitation, ainsi que l'historique de progression.

          Nous nous basons sur l'équation de Bellman pour mettre à jour la table de Q-Learning :

          README-1667658736226.png

          La Qtable est formé sous forme de dictionnaire récursif de N+1 dimension, N étant le nombre total de lignes visible. L'idée est de fusionner ensemble les clés communes afin de ne pas consommer trop de mémoire vive (on peut avoir plus de 5 millions de clés différentes). La première dimension représente la première ligne visible, la deuxième dimension la seconde…, La dernière dimension représente les actions possibles.