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.
- Naissance : la machine d'états est créée une seule fois.
- Perception : à chaque tour, le fantôme mesure la distance à Pacman et la traduit en événement.
- Action : 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
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 → | pacmanLoin | pacmanEnVue | pacmanProche |
|---|---|---|---|
| SEUL | ✓ | VUE | PROCHE |
| 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 - 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.
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.
# --- 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).
# --- 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 :
- Quels sont les deux états ? Quels événements font passer de l'un à l'autre ?
- L'action dépend maintenant de deux états : combien de cases a le tableau des comportements ?
- 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 - 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
- La console affiche un changement d'état quand Pacman approche, puis quand il s'éloigne.
- Les trois états apparaissent, et le fantôme redevient SEUL quand Pacman est loin.
- Le changement d'état ne s'affiche qu'une fois (pas à chaque tour).
- Avec
-n 3, chaque partie recommence à SEUL. - Bonus : en mode « chasse aux fantômes » (fantôme blanc), il ne fonce plus sur Pacman.
Retour à l'énoncé des laboratoires Agents · voir aussi la version complète