👻 Brancher une machine d'états sur un fantôme

Série noire - l'agent déterministe. Labo Pacman, étape 3 : un cerveau (la machine d'états) branché sur un corps (le fantôme).

Version indices - à vous de jouer

Le principe : trois actes, toujours dans cet ordre

Un agent déterministe ne réfléchit pas : il perçoit, son état change, il agit selon son état. Chaque décision se lit dans un tableau, rien n'est laissé au hasard du code.

  1. Naissance (le cerveau) : la machine d'états est créée une seule fois.
  2. Perception (le nerf optique) : à chaque tour, le fantôme mesure la distance à Pacman et la traduit en événement.
  3. Action (le muscle) : l'état actif dicte le comportement.

Où ça se branche dans le Pacman

À chaque tour, le jeu appelle getDistribution(state) de chaque fantôme (une sous-classe de GhostAgent, dans ghostAgents.py). La méthode retourne une distribution (util.Counter) : une note par action permise, et le jeu tire l'action au sort selon ces notes. Une seule action à 1.0 = une décision certaine.

Le nom du fichier doit finir par Agents.py (fantomesIntelligentsAgents.py) : pacman.py ne cherche les classes passées à -g que dans ces fichiers.

La machine : SEUL, VUE, PROCHE

? ? ? ? ? ? SEUL VUE PROCHE

PROCHE : Pacman à 2 cases ou moins. VUE : de 3 à 5 cases. SEUL : plus de 5 cases (distance de Manhattan). Une ligne de la matrice est remplie en exemple ; à vous les deux autres, et les événements du diagramme.

état actuel ↓ / événement →pacmanLoinpacmanEnVuepacmanProche
SEUL✓VUEPROCHE
VUE???
PROCHE???

✓ = l'état ne change pas.

Votre librairie du labo Machines d'états fait l'affaire. Si vous repartez de zéro, voici l'idée : le dictionnaire transitions est la matrice état-transition. Il reste une méthode à écrire.

machineEtats.py squelette : la méthode declencher est à écrire
# machineEtats.py - votre machine d'états, sans librairie (reprenez celle du labo Machines d'états)
# Idée : la machine se décrit comme sa matrice état-transition
#   {(etatActuel, evenement): nouvelEtat}   une case absente = l'état ne change pas


class MachineEtats:

    def __init__(self, nom, etatInitial, transitions):
        self.nom = nom
        self.etat = etatInitial
        self.transitions = transitions

    def declencher(self, evenement):
        # TODO : chercher (self.etat, evenement) dans self.transitions
        #        - trouvé : changer self.etat et l'afficher en console (utile pour déboguer)
        #        - absent : ne rien faire (c'est le ✓ de la matrice, pas une erreur)
        pass

1Naissance : le cerveau

La machine se crée dans __init__, une seule fois. Recréée dans getDistribution, elle repartirait à SEUL à chaque tour : le fantôme deviendrait amnésique.

fantomesIntelligentsAgents.py où naît le cerveau
class FantomeIntelligent(GhostAgent):

    # --- Acte 1 : la naissance du cerveau ---
    def __init__(self, index):
        super().__init__(index)
        # TODO : créer la machine ICI, une seule fois (pas dans getDistribution)
        #        états SEUL, VUE, PROCHE ; état initial SEUL ; transitions = votre matrice
        self.machineDistance = MachineEtats(f"fantome-{index}", "SEUL", {})

Piège : avec -n 3, le même objet fantôme joue les 3 parties. Le jeu appelle registerInitialState(state) au début de chaque partie : c'est là qu'on remet la machine à zéro.

2Perception : le nerf optique

La distance se traduit en un événement par tour, et toutes les distances doivent en produire un. Oublier « loin » : le fantôme ne redevient jamais SEUL.

fantomesIntelligentsAgents.py à compléter
    # --- Acte 2 : la perception ---
    def percevoir(self, state):
        positionFantome = state.getGhostPosition(self.index)
        distancePacman = manhattanDistance(positionFantome, state.getPacmanPosition())
        # TODO : traduire la distance en UN événement par tour, pour TOUTES les distances
        #        (proche, en vue... et loin : sinon le fantôme ne redevient jamais SEUL)
        #        puis self.machineDistance.declencher(evenement)

Piège de la librairie transitions : déclencher un événement qui n'a pas de transition depuis l'état actuel (par exemple pacman_proche() quand on est déjà PROCHE) lève une MachineError et la partie plante. Notre machine, elle, l'ignore : c'est le ✓ de la matrice.

3Action : le muscle

Pour poursuivre ou fuir : essayer chaque action permise, calculer la distance à Pacman qu'elle donnerait, garder la plus petite (poursuivre) ou la plus grande (fuir).

fantomesIntelligentsAgents.py à compléter
    # --- Acte 3 : l'action ---
    def getDistribution(self, state):
        self.percevoir(state)
        actionsPermises = state.getLegalActions(self.index)
        distribution = util.Counter()
        # TODO : choisir selon self.machineDistance.etat (SEUL = au hasard, PROCHE = fuir...)
        #        pour fuir ou poursuivre : essayer chaque action et mesurer la distance qui en résulte
        #        (indice : Actions.directionToVector(action) donne le déplacement (dx, dy))
        for action in actionsPermises:
            distribution[action] = 1.0
        distribution.normalize()
        return distribution   # le jeu attend une distribution, pas une action

Piège : return action ne marche pas. getDistribution doit retourner une distribution ; le jeu fait lui-même le tirage (util.chooseFromDistribution).

Pour aller plus loin : doubler la machine

Quand Pacman mange une capsule, le jeu met scaredTimer à 40 pour chaque fantôme (state.getGhostState(self.index).scaredTimer) : le fantôme devient blanc et Pacman peut le manger. Une deuxième machine, avec deux états, peut retenir ce mode. Questions pour la concevoir :

  1. Quels sont les deux états ? Quels événements font passer de l'un à l'autre ?
  2. L'action dépend maintenant de deux états : combien de cases a le tableau des comportements ?
  3. Un fantôme VUE doit-il poursuivre Pacman quand il est blanc ?

Le squelette complet, qui se lance déjà

Les deux fichiers vont dans le dossier du Pacman (à côté de pacman.py), puis : python3 pacman.py -g FantomeIntelligent -l mediumClassic. Tel quel, le fantôme bouge au hasard : on part d'un programme qui marche et on le rend intelligent, TODO par TODO.

fantomesIntelligentsAgents.py testé : se lance tel quel
# fantomesIntelligentsAgents.py - squelette d'un fantôme intelligent, piloté par une machine d'états
# Le nom du fichier finit par « Agents.py » : sinon pacman.py ne trouve pas la classe.
# Lancer : python3 pacman.py -g FantomeIntelligent -l mediumClassic
# Tel quel, il se lance : le fantôme bouge au hasard. À vous de lui donner un cerveau.

import util
from game import Actions
from ghostAgents import GhostAgent
from machineEtats import MachineEtats
from util import manhattanDistance


class FantomeIntelligent(GhostAgent):

    # --- Acte 1 : la naissance du cerveau ---
    def __init__(self, index):
        super().__init__(index)
        # TODO : créer la machine ICI, une seule fois (pas dans getDistribution)
        #        états SEUL, VUE, PROCHE ; état initial SEUL ; transitions = votre matrice
        self.machineDistance = MachineEtats(f"fantome-{index}", "SEUL", {})

    # --- Acte 2 : la perception ---
    def percevoir(self, state):
        positionFantome = state.getGhostPosition(self.index)
        distancePacman = manhattanDistance(positionFantome, state.getPacmanPosition())
        # TODO : traduire la distance en UN événement par tour, pour TOUTES les distances
        #        (proche, en vue... et loin : sinon le fantôme ne redevient jamais SEUL)
        #        puis self.machineDistance.declencher(evenement)

    # --- Acte 3 : l'action ---
    def getDistribution(self, state):
        self.percevoir(state)
        actionsPermises = state.getLegalActions(self.index)
        distribution = util.Counter()
        # TODO : choisir selon self.machineDistance.etat (SEUL = au hasard, PROCHE = fuir...)
        #        pour fuir ou poursuivre : essayer chaque action et mesurer la distance qui en résulte
        #        (indice : Actions.directionToVector(action) donne le déplacement (dx, dy))
        for action in actionsPermises:
            distribution[action] = 1.0
        distribution.normalize()
        return distribution   # le jeu attend une distribution, pas une action

Pour vérifier votre fantôme

Retour à l'énoncé des laboratoires Agents · voir aussi la version complète