Utilisation de PyQt pour une colonie de fourmis

Préambule

Il y a quelque temps déjà, je me suis mis à Qt avec Python. J'avais trouvé peu de tutoriels, et quasi rien en français (et je ne parle même pas de PyKde, mais ça, c'est une autre histoire…). Même la doc de PyQt, qui est basée sur celle de C++1, contient des bouts de code de C++2 ! Je m'étais alors dit que si je faisais du PyQt, j'essayerai d'en faire un tuto, ou au moins d'expliquer un peu mon code. Puis en voyant je ne sais plus quelle vidéo de petits robots « intelligents », je me suis dit que ce serait sympa que je me fasse un programme de colonies de fourmis. Le but était surtout de faire du PyQt, mais avec un objectif sympatique : des fourmis. Un peu débiles puisque la théorie des colonies de fourmis est quand même assez poussée.

Ça fait un peu plus d'un an et demi que j'ai fait ces programmes, mais je pensais à en faire un article. Du coup, les 4-5 soirs où j'ai travaillé dessus, j'ai fait à chaque fois une nouvelle « version ». Je vais donc commenter le code de ces versions en plusieurs fois pour essayer d'expliquer le code de A à Z. Je pars par contre du principe que vous savez coder orienté objet en python, sinon, pour débuter, je vous conseille d'aller faire un tour sur le net, en commençant par exemple par le site de sam et max. Pour du PyQt une intro est disponible sur learningpython.com et un bon livre sur commandprompt.com.

Première ébauche

Je tiens d'abord à signaler que ce n'est pas forcément bien codé algorithmiquement parlant ; ce n'était pas le but je le rappelle. De plus, comme c'est du vieux code, et que j'avais pas forcément commenté, je louperai peut-être des choses à expliquer :þ (surtout que comme j'étais mal parti, j'ai changé pas mal de choses en cours de route). Oh, et c'est du python2, pas du 3.

Je rappelle également que mon mail est ouvert, et que je posterai un lien vers identi.ca sur lequel vous pouvez répondre également.

Rentrons dans le vif du sujet. Pour faire mon programme, je voulais une classe fourmi (ant) et une classe pour l'affichage (display). Plus une classe d'entrée (simu) qui permettera d'instancier tout ça.

Le principe sera le suivant : on maintient un tableau à deux dimensions à jour. Celui-ci contiendra nos fourmis, la nourriture, le point de départ… Cette grille sera passée en paramètre de la classe display pour pouvoir faire l'affichage et de la classe ant pour que la fourmi connaisse son environnement (enfin, presque…).

Commençons par l'affichage.

display.py

Pour afficher notre grille, on va utiliser un QWidget tout bête et réimplémenter la méthode paintEvent. Pour chaque « case » de la grille, on dessinera un rectangle.

class GridDisplay(QtGui.QWidget):
    """
    Affichage des fourmis, de la nourriture, etc.
    """

    def __init__(self, grille):
        """ initialisation """
        super(GridDisplay, self).__init__()
        self.grille = grille
        self.coordXmax = len(self.grille)
        self.coordYmax = len(self.grille[0])

    def paintEvent(self, event):
        """re-implementation de la fonction paintEvent()"""
        painter = QtGui.QPainter()
        painter.begin(self)

        w = self.size().width()
        h = self.size().height()
        x = self.coordXmax
        y = self.coordYmax

        for i in range(x):
            for j in range(y):
                c = self.grille[i][j]
                painter.setBrush(QtGui.QBrush(QtGui.QColor(c, c, c))) #Comment c'est rempli
                painter.setPen(QtGui.QPen(QtGui.QColor("white"))) #Couleur des bords
                painter.drawRect(i*w/x, j*h/y, w/x, h/y)

        painter.end()

QPainter est la classe qui permet de dessiner sur les widgets. Normalement painter.begin(self) renvoie un booléen indiquant si oui ou non, on a réussi à commencer à peindre (de même pour end()). On dit de quelle couleur peindre avec painter.setBrush(QtGui.QBrush(QtGui.QColor(c, c, c))) puis on dessine notre rectangle avec drawRect(abscisse, ordonnée, hauteur, largeur).

On met tout ça dans un fichier display.py et pour tester, on se fait une grille aléatoire :

if __name__ == '__main__':
    """Pour tester"""
    from random import randint
    app = QtGui.QApplication([])
    xmax = 10
    ymax = 5
    grille = [[randint(0, 255) for _ in range(ymax)][:] for _ in range(xmax)]
    grd = GridDisplay(grille)
    grd.show()
    app.exec_()

Le if __name__ = 'main':= permet d'exécuter le code qui suit uniquement s'il n'est pas appelé en tant que module (c'est-à-dire si on fait python display.py).

Rien de bien compliqué sinon : on crée notre application avec app = QtGui.QApplication([]), notre grille, notre widget, que l'on affiche, et on lance l'application avec app.exec_().

ant.py

Passons à la fourmi. Pour pouvoir retourner à sa fourmilière, une fourmi a besoin de connaître le chemin qu'elle a fait. Il faut également que l'on connaisse ses coordonnées, ainsi que la fameuse grille. Pour instancier une fourmi, on lui donnera également ses coordonnées de départ :

class Ant:
    """
    Ce qui définit une fourmi et ce qu'elle sait faire
    """
    def __init__(self, parent, posx, posy):
        """
        Une fourmi
        """
        self.coord = [posx, posy]
        self.pathDone = [self.coord]
        self.parent = parent

pathDone contiendra la liste des coordonnées de la grille par lesquelles la fourmi est passée.

À chaque itération, une fourmi ira sur une des huit cases adjacentes, selon son état (si elle a trouvé de la nourriture, si elle doit rentrer, etc.). Pour l'instant, elle va juste aller sur une case :

def move(self):
    """
    Un pas, selon l'environnemnt.
    """
    directions = [(-1, -1), (-1, 0), (-1, 1),
                 (0, -1), (0, 1),
                 (1, -1), (1, 0), (1, 1)]
    direction = directions[randint(0,7)]
    self.coord[0] += direction[0]
    self.coord[0] %= self.parent.coordXmax
    self.coord[1] += direction[1]
    self.coord[1] %= self.parent.coordYmax
    self.pathDone += [self.coord[:]]

À la fin de la fonction, on ajoute donc les nouvelles coordonnées.

De nouveau, on va tester notre classe, pour avoir confirmation que nos fourmis se déplace bien. Pour cela, on a besoin d'un affichage de test, ou plutôt de ses coordonnées :

class PourTest:
    """
    Une grille pour tester
    """
    def __init__(self, maxX, maxY):
        """ les coordonnées max de la grille"""
        self.coordXmax = maxX
        self.coordYmax = maxY


if __name__ == '__main__':
    """
    Pour tester
    """
    xmax = 10
    ymax = 5
    grille = [[0 for _ in range(ymax)][:] for _ in range(xmax)] #on initialise la grille
    parent = PourTest(xmax, ymax) #pour l'affichage
    nAnts = 10
    lAnts = [] #liste de nos fourmis
    for _ in range(nAnts): #pour i de 0 à 9
        fourmi = Ant(parent, randint(0, xmax - 1), randint(0, ymax - 1)) #on crée une fourmi
        grille[fourmi.coord[0]][fourmi.coord[1]] += 1 #on l'ajoute à la grille
        lAnts.append(fourmi) #et à la liste 
    niterations = 10
    from time import sleep
    for _ in range(niterations):
        #affichage (à -90°) de la grille 
        for ligne in grille:
            print ligne
        #pour chaque fourmi de la liste
        for fourmi in lAnts:
            ##suppression de la fourmi dans la grille
            grille[fourmi.coord[0]][fourmi.coord[1]] -= 1
            ##déplacement de la fourmi
            fourmi.move()
            ##ajout de la fourmi dans la grille
            grille[fourmi.coord[0]][fourmi.coord[1]] += 1
        sleep(1)
        print "=" * 40

simu.py

Maintenant qu'on a testé nos deux classes, on va les assembler. Notre classe principale va donc : instancier une grille (et la placer en widget central), des fourmis (avec le widget central comme parent), et périodiquement, on met à jour la grille.

def __init__(self):
    """ Initialisation """
    super(Simu, self).__init__()

    #coordonnées + grille
    self.xmax = 50
    self.ymax = 50
    self.grille = [[0 for _ in range(self.ymax)][:] for _ in range(self.xmax)]

    #le widget central est notre affichage de la grille
    self.centralWidget = GridDisplay(self.grille)
    self.setCentralWidget(self.centralWidget)
    self.resize(480, 480)

    #on initialise nos fourmis
    self.nAnts = 30
    self.lAnts = []
    for _ in range(self.nAnts):        
        fourmi = Ant(self.centralWidget, randint(0, self.xmax - 1), randint(0, self.ymax - 1))
        self.grille[fourmi.coord[0]][fourmi.coord[1]] += 1
        self.lAnts.append(fourmi)

    #A chaque top du timer, on actualise la grille
    self.timer = QTimer()
    self.connect(self.timer, SIGNAL("timeout()"), self.animate)
    self.compteur = 0
    #C'est parti pour toutes les 10ms
    self.timer.start(10)

Assez fréquemment on met à jour la grille en faisant bouger les fourmis. Ensuite, on met la grille dans le widget central, et on l'update pour appeler paintEvent

def animate(self):
    """maj de la grille"""
    self.compteur += 1
    for fourmi in self.lAnts:
        #déplacement de la fourmi
        ##suppression de la fourmi dans la grille
        self.grille[fourmi.coord[0]][fourmi.coord[1]] -= 1
        ##déplacement de la fourmi
        fourmi.move()
        self.grille[fourmi.coord[0]][fourmi.coord[1]] += 1
    self.centralWidget.grille = self.grille
    self.centralWidget.update()

    if self.compteur > 50:
        self.timer.stop()

Postambule

La suite sera un peu plus intéressante (et les fourmis aussi). Le code sera disponible sur bitbucket.org au fur et à mesure des épisodes. Pour tout télécharger d'un coup, c'est dans Downloads>Branches.

Footnotes:

1

http://www.riverbankcomputing.com/static/Docs/PyQt4/html/classes.html : Because this is based on the Qt C++ documentation it still contains C++ code fragments, broken links etc. These will be fixed in future releases.

2

C'est le cas pour la majorité des exemples j'ai l'impression, même si on trouve des bouts de python pour les classes principales : http://www.riverbankcomputing.co.uk/static/Docs/PyQt4/html/qimage.html#QImage-8

Autres billets

Date: <2013-01-19>

Generated by Emacs 24.3.1 (Org mode 8.2.4) - Show Org source (htmlized)

CSS inspired by Tontof, colors by Chaotic Soul

Validate