Un Pac-Man sur ta Numworks, en python

Projets

Rond comme un ballon, et plus jaune qu’un citron, c’est lui Pac-Man ! Pac-Man débarque sur ta NumWorks ! Aide le à manger toutes les pac-gommes en évitant les fantômes et accumule le plus grand score.

Bon jeu !

URL courte de cet article : nsi.xyz/pacman

Pac-Man débarque sur ta NumWorks !

Aide le à manger toutes les pac-gommes en évitant les fantômes et accumule le plus grand score. Pour cela, tu peux :

  • Manger des pac-gommes (10)
  • Manger des super pac-gommes (50)
  • Manger les fantômes (200, 400, 800, puis 1600)

Si tu manges tout les fantômes a chaque super pac-gommes, tu obtiendras même un bonus de 12000 points !

Bon jeu !

Pac-Man, un sacré projet…

Comme toujours, sur nsi.xyz, on aime raconter l’histoire derrière nos projets.

Celle là commence dans une cuisine. Je commençais à faire à manger quand le patron  m’a envoyé un message concernant un bug sur un de ses projets « un jeu en une soirée » : Treasure

L’idée me plaisait, j’avais envie de faire pareil. Mais quel jeu choisir ?

🤓  Je vais tenter de faire un jeu en une nuit aussi vous m’avez motivé. Une idée ?
🧐 Pacman, tout autre jeu serait trop facile pour toi 😅

Pac-Man, ca paraissait infaisable… Des IA différentes pour chaque fantômes, des collisions, des tableaux et encore des tableaux, tout ça dans 32Ko max !

On est parti !

Première étape : Comment stocker la carte ?

Déjà, posons nous la question suivante : C’est quoi la carte dans Pac-Man ?

La carte, c’est soit :

  • Les chemins : On peut se balader dessus, les fantômes aussi.
  • Les murs : Comme on peut le penser, c’est l’inverse des chemins, on ne peut pas les traverser.

Du coup, on est sauvé ! Le tableau contiendra uniquement des 1 (pour les murs) ou des 0 (pour les chemins). Ainsi, on passe d’un tableau 2D de Booléen à un tableau 1D d’entier.
L’astuce est simple mais efficace : Une ligne de 1 et de 0 peut être convertie en entier grâce au fait que l’ordinateur code les entier en binaire.
En plus de réduire la complexité en espace, ça réduit aussi celle en temps, ce qui est pratique quand on parle d’un jeu tel que Pac-Man sur une calculatrice.

Deuxième étape : Construire la carte

Je commence déjà par choisir une taille pour les cases : 10px.
Sachant que la NumWorks dispose d’un écran de 222 px de haut, je sais que ma carte fera 22 cases de haut.Retour ligne automatique
A l’aide d’un logiciel professionnel et d’une image sur Internet, je crée une image blanche de 18x22px (Pourquoi 18 ? Je ne sais pas) que je commence à colorier de façon à obtenir ceci :

Et cette carte se traduit par le tableau suivant :

bits = 18  # Le nombre de bits pour chaque entiers, ce sera utile plus tard
terrain = (262143,131841,187245,187245,131073,186285,135969,252783,249903,251823,1152,251823,249903,251823,131841,187245,147465,219051,135969,195453,131073,262143)

Encore une fois, on utilise des tuple pour gagner en mémoire.

Maintenant, il ne reste plus qu’a rendre la carte à l’écran.
Et pour ca, on utilise une belle fonction :

def render():
    global terrain
    for l in range(len(terrain)):
        for c in range(bits):
            fill_rect(c*10+140,l*10+1,10,10,colors[0])
            if terrain[l]>>(bits-1-c) & 1:
                for d in ((1,0),(0,1),(-1,0),(0,-1)):
                    if 0 <= l + d[0] <= len(terrain) - 1 and 0 <= c + d[1] <= bits - 1 and not terrain[l + d[0]]>>(bits-1-(c+d[1])) & 1:
                        fill_rect(c*10+140+9*(d[1]==1),l*10+1+9*(d[0]==1),1+9*(d[1]==0),1+9*(d[0]==0),colors[1])

Pour faire simple, cette fonction lit les entier comme s’ils était des tableaux de 1 et de 0. Pour chaque case, elle dessine un carré noir. Si la case est un 1, elle regarde dans les 4 direction possibles, si un 0 s’y trouve, alors elle dessine un mur de ce côté.

En rajoutant une petite fonction pour du pré-rendu que je ne détaillerai pas, on obtiens déjà ceci :

Troisième étape : Ajouter de « la classe » au jeu

Outre l’aspect douteux de ce jeu de mot, il est temps de créer une classe pour Pac-Man et ses amis.
Ainsi née la classe Entity qui contiendra, à elle seule, toutes les méthodes pour le jeu. En effet, si on ne considère pas l’entrée des directions, les fantômes et Pac-Man se déplacent de la même manière sur la carte.

Pour faire court, cette classe contient 4 méthodes :

  • Le constructeur : initialise tout les attributs de l’entité.
  • espace : C’est la méthode qui teste les collision, grâce à elle, les fantômes et Pac-Man ne tournent que sur les intersections sans déborder sur les murs. Et évidemment, ils ne traversent pas non plus les murs.
  • move : C’est la méthode qui fait se déplacer les entités, qui réactualise les pac-gommes et super pac-gommes (j’y viendrai plus tard) et qui gère les collisions Pac-Man / Fantômes et Pac-Man / pac-gommes.
  • ia : Détermine la direction des fantômes.

Attardons-nous sur la méthode ia :

def ia(self,x,y):
    if self.f:
        while True:
            d = randint(0,3)
            dx, dy = ((-0.1,0),(0,-0.1),(0,0.1),(0.1,0))[d]
            if d != (3,2,1,0)[self.d] and self.espace(dx,dy):
                self.d = d
                break
    else:
        distances = [9999 for _ in range(4)]
        for i in range(4):
            if i != (3,2,1,0)[self.d]:
                dx, dy = ((-0.1,0),(0,-0.1),(0,0.1),(0.1,0))[i]
                if self.espace(dx,dy):
                    distances[i] = sqrt((self.y + dy - y)**2 + (self.x + dx - x)**2)
        self.d = distances.index(min(distances))

On voit qu’elle comporte deux parties, c’est un des éléments clés de Pac-Man : quand les fantômes sont dans l’état effrayé, ils choisissent une direction au hasard, sinon, ils choisissent la direction qui leur permet d’avoir la plus courte distance euclidienne à un point. Rajoutons que les fantômes n’ont pas le droit de faire demi-tour.

Mais une question se pose : Quel est donc ce point que visent les fantômes ?

Quatrième étape : Les fantômes débarquent !

Dans Pac-Man, il y a quatre fantômes. Mais il n’y a pas que leur couleur qui change, leur comportement aussi !

Petit listing exhaustif :

Blinky poursuit Pac-Man, ainsi le point qu’il cherche à atteindre et le point au centre de Pac-Man.

Pinky est la plus intelligente, ainsi le point qu’elle cherche à atteindre est deux cases devant Pac-Man dans la direction qu’il regarde.

Inky est un peu plus farceur, pour savoir ou il vise, il faut prendre en compte la position de Blinky et de Pac-Man. Posons v le vecteur Blinky-Point deux cases devant Pac-Man dans la direction qu’il regarde, Inky visera le point situé à 2v, ce qui le rend imprévisible.

Clyde quant à lui est un froussard, il agit comme Blinky quand à lui, est à 4 « mètres » de Pac-Man, mais vise un angle de la carte quand il est proche de Pac-Man.

Cinquième étape : Déplacer Pac-Man

Les fantômes maintenant capables de changer de direction, il n’en n’est rien pour Pac-Man…Retour ligne automatique
Comment se déplace Pac-Man ? Pac-Man se déplace jusqu’au bout d’un chemin avant de s’arrêter. Si on lui donne une direction, il la prendra dès qui le pourra sauf si la direction en question est la direction opposée à celle qu’il emprunte déjà.

Du coup, avec nos méthodes et un peu d’astuce, c’est tout simple !

for i in range(4):
    if keydown(i):
        if i == (3,2,1,0)[pacman.d]:
            pacman.d = i
        pacman.nd = i
if pacman.espace():
    pacman.d = pacman.nd

Sixième étape : Gestion des pac-gommes

Vous l’aurez peut-être remarqué mais le jeu est relativement fluide et rapide.Retour ligne automatique
Tout ça repose sur une grosse optimisation sur les pac-gommes, sans cette optimisation, le jeu tourne à 4 IPS.

Une question importante se pose : Comment réussir à afficher et rafraichir les pac-gommes ?

Une première approche innocente serait de stocker les pac-gommes sous forme de tableau de 0, 1 et 2 et le parcourir en affichant ou non les précieuses gommes. Sauf que là, bonjour le temps pour chaque image…

Une deuxième approche est de créer deux listes d’entier et d’utiliser les opérateurs binaires et les soustractions pour stocker les pac-gommes.Retour ligne automatique
Enfin, et c’est la l’énorme gain de temps. Pour chaque entités, après un mouvement, on regarde dans les deux listes si une (super) pac-gomme se trouve sur la case précédente, que ce soit un mur ou non, et on dessine une pac-gomme si besoin. Et c’est comme ça qu’on atteint une fluidité de jeu.

En plus, au lieu d’un parcours de liste, on a juste a implémenter les cinq lignes suivantes :

px, py = int(self.x - 5.5*dx), int(self.y - 5.5*dy)
if pacgommes[py]>>(bits-1-px) & 1:
    fill_rect(px*10+144,py*10+5,2,2,(250, 207, 173))
if superpacgommes[py]>>(bits-1-px) & 1:
    fill_rect(px*10+143,py*10+4,4,4,(250, 207, 173))

Septième étape : Gestion du score et des états des fantômes

Pour le score, on a juste à implémenter les règles citées en début d’article.

Pour la gestion des états des fantômes, c’est un peu plus compliqué qu’une simple variable.
Il faut rajouter un attribut afin que lorsqu’on mange un fantôme, il ne soit plus effrayé sans pour autant que les autres ne le soient plus.
Enfin, il faut rajouter une clock qui nous permettra de rendre leur état normal au fantômes après un certain temps en le rendant blanc juste avant pour alerter le joueur.

Conclusion

Evidemment, je ne me suis pas attardé sur tous les détails car se serait beaucoup trop long. Mais ce sont les éléments principaux pour un Pac-Man.
Je n’ai pas rajouté les changement de phases entre Chase et Scatter, de peur d’impacter les performances ou de dépasser la place disponible en mémoire.
Le Pac-Man fini rend bien :

Code jouable disponible ici :