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).
| état actuel ↓ / événement → | pacmanLoin | pacmanEnVue | pacmanProche |
|---|---|---|---|
| SEUL | ✓ | VUE | PROCHE |
| VUE | SEUL | ✓ | PROCHE |
| PROCHE | SEUL | VUE | ✓ |
✓ = l'état ne change pas.
Notre librairie tient en 15 lignes : le dictionnaire transitions est
la matrice état-transition. Une case absente veut dire « l'état ne change pas » : déclencher un
événement sans transition n'est pas une erreur.
# machineEtats.py - notre machine d'états, sans librairie
# 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):
nouvelEtat = self.transitions.get((self.etat, evenement), self.etat)
if nouvelEtat != self.etat:
print(f"[{self.nom}] {self.etat} --{evenement}--> {nouvelEtat}")
self.etat = nouvelEtat
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)
self.creerMachines()
def registerInitialState(self, state):
# Le jeu l'appelle au début de CHAQUE partie (avec -n 3, le même fantôme joue 3 parties)
self.creerMachines()
def creerMachines(self):
self.machineDistance = MachineEtats(f"fantome-{self.index}", "SEUL", {
("SEUL", "pacmanEnVue"): "VUE",
("SEUL", "pacmanProche"): "PROCHE",
("VUE", "pacmanLoin"): "SEUL",
("VUE", "pacmanProche"): "PROCHE",
("PROCHE", "pacmanLoin"): "SEUL",
("PROCHE", "pacmanEnVue"): "VUE",
})
self.machineMode = MachineEtats(f"fantome-{self.index}-mode", "NORMAL", {
("NORMAL", "capsuleMangee"): "CHASSE",
("CHASSE", "peurTerminee"): "NORMAL",
})
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())
if distancePacman <= DISTANCE_PROCHE:
self.machineDistance.declencher("pacmanProche")
elif distancePacman <= DISTANCE_VUE:
self.machineDistance.declencher("pacmanEnVue")
else:
self.machineDistance.declencher("pacmanLoin")
if state.getGhostState(self.index).scaredTimer > 0:
self.machineMode.declencher("capsuleMangee")
else:
self.machineMode.declencher("peurTerminee")
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)
comportement = COMPORTEMENTS[(self.machineMode.etat, self.machineDistance.etat)]
distribution = util.Counter()
if comportement == "auHasard":
for action in actionsPermises:
distribution[action] = 1.0
else:
distribution[self.choisirAction(state, actionsPermises, comportement)] = 1.0
distribution.normalize()
return distribution # le jeu attend une distribution, pas une action
def choisirAction(self, state, actionsPermises, comportement):
positionFantome = state.getGhostPosition(self.index)
positionPacman = state.getPacmanPosition()
vitesse = 0.5 if state.getGhostState(self.index).scaredTimer > 0 else 1.0
distances = {}
for action in actionsPermises:
deplacementX, deplacementY = Actions.directionToVector(action, vitesse)
positionSuivante = (positionFantome[0] + deplacementX, positionFantome[1] + deplacementY)
distances[action] = manhattanDistance(positionSuivante, positionPacman)
if comportement == "poursuivre":
cible = min(distances.values())
else:
cible = max(distances.values())
return random.choice([action for action, distance in distances.items() if distance == cible])
Piège : return action ne marche pas. getDistribution doit retourner
une distribution ; le jeu fait lui-même le tirage (util.chooseFromDistribution).
Doubler la machine : la chasse aux fantômes
Quand Pacman mange une capsule, le jeu met scaredTimer à 40 : le fantôme devient blanc
et Pacman peut le manger. Une deuxième machine retient ce mode ; l'action dépend des deux
états à la fois.
| mode ↓ / événement → | capsuleMangee | peurTerminee |
|---|---|---|
| NORMAL | CHASSE | ✓ |
| CHASSE | ✓ | NORMAL |
| action (mode + état) | SEUL | VUE | PROCHE |
|---|---|---|---|
| NORMAL | au hasard | poursuivre | fuir |
| CHASSE | au hasard | fuir | fuir |
À discuter : le fantôme qui hésite
La consigne indicative « PROCHE = FUIR » donne un fantôme qui oscille : il poursuit Pacman (VUE),
arrive à 2 cases, fuit (PROCHE), repasse VUE et repart... La console le montre :
VUE --pacmanProche--> PROCHE puis PROCHE --pacmanEnVue--> VUE.
Changez une seule ligne : ("NORMAL", "PROCHE"): "poursuivre". Le fantôme
attaque, et ne fuit plus qu'en mode CHASSE. Le comportement se modifie dans le tableau, sans toucher
au reste du code : c'est tout l'intérêt d'une machine d'états.
Le fichier complet, prêt à lancer
Les deux fichiers vont dans le dossier du Pacman (à côté de pacman.py), puis :
python3 pacman.py -g FantomeIntelligent -l mediumClassic. Ajouter
-p GreedyAgent pour qu'un Pacman automatique joue, -n 3 pour 3 parties.
La console affiche chaque changement d'état : [fantome-1] SEUL --pacmanEnVue--> VUE.
# fantomesIntelligentsAgents.py - un fantôme intelligent, piloté par deux machines 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
import random
import util
from game import Actions
from ghostAgents import GhostAgent
from machineEtats import MachineEtats
from util import manhattanDistance
DISTANCE_PROCHE = 2 # PROCHE : 2 cases ou moins
DISTANCE_VUE = 5 # VUE : de 3 à 5 cases ; au-delà, SEUL
# Ce que fait le fantôme selon (mode, état)
COMPORTEMENTS = {
("NORMAL", "SEUL"): "auHasard",
("NORMAL", "VUE"): "poursuivre",
("NORMAL", "PROCHE"): "fuir", # consigne de l'énoncé : PROCHE = FUIR
("CHASSE", "SEUL"): "auHasard",
("CHASSE", "VUE"): "fuir", # Pacman a mangé une capsule : il peut nous manger
("CHASSE", "PROCHE"): "fuir",
}
class FantomeIntelligent(GhostAgent):
# --- Acte 1 : la naissance du cerveau ---
def __init__(self, index):
super().__init__(index)
self.creerMachines()
def registerInitialState(self, state):
# Le jeu l'appelle au début de CHAQUE partie (avec -n 3, le même fantôme joue 3 parties)
self.creerMachines()
def creerMachines(self):
self.machineDistance = MachineEtats(f"fantome-{self.index}", "SEUL", {
("SEUL", "pacmanEnVue"): "VUE",
("SEUL", "pacmanProche"): "PROCHE",
("VUE", "pacmanLoin"): "SEUL",
("VUE", "pacmanProche"): "PROCHE",
("PROCHE", "pacmanLoin"): "SEUL",
("PROCHE", "pacmanEnVue"): "VUE",
})
self.machineMode = MachineEtats(f"fantome-{self.index}-mode", "NORMAL", {
("NORMAL", "capsuleMangee"): "CHASSE",
("CHASSE", "peurTerminee"): "NORMAL",
})
# --- Acte 2 : la perception ---
def percevoir(self, state):
positionFantome = state.getGhostPosition(self.index)
distancePacman = manhattanDistance(positionFantome, state.getPacmanPosition())
if distancePacman <= DISTANCE_PROCHE:
self.machineDistance.declencher("pacmanProche")
elif distancePacman <= DISTANCE_VUE:
self.machineDistance.declencher("pacmanEnVue")
else:
self.machineDistance.declencher("pacmanLoin")
if state.getGhostState(self.index).scaredTimer > 0:
self.machineMode.declencher("capsuleMangee")
else:
self.machineMode.declencher("peurTerminee")
# --- Acte 3 : l'action ---
def getDistribution(self, state):
self.percevoir(state)
actionsPermises = state.getLegalActions(self.index)
comportement = COMPORTEMENTS[(self.machineMode.etat, self.machineDistance.etat)]
distribution = util.Counter()
if comportement == "auHasard":
for action in actionsPermises:
distribution[action] = 1.0
else:
distribution[self.choisirAction(state, actionsPermises, comportement)] = 1.0
distribution.normalize()
return distribution # le jeu attend une distribution, pas une action
def choisirAction(self, state, actionsPermises, comportement):
positionFantome = state.getGhostPosition(self.index)
positionPacman = state.getPacmanPosition()
vitesse = 0.5 if state.getGhostState(self.index).scaredTimer > 0 else 1.0
distances = {}
for action in actionsPermises:
deplacementX, deplacementY = Actions.directionToVector(action, vitesse)
positionSuivante = (positionFantome[0] + deplacementX, positionFantome[1] + deplacementY)
distances[action] = manhattanDistance(positionSuivante, positionPacman)
if comportement == "poursuivre":
cible = min(distances.values())
else:
cible = max(distances.values())
return random.choice([action for action, distance in distances.items() if distance == cible])
Retour à l'énoncé des laboratoires Agents · voir aussi la version indices