FAOD-DUI4/maze/maze.md

11 KiB
Raw Blame History

title author date
TP - Structure de données linéaire - La pile TODARO Cédric 06 juin 2021

Introduction

Cette activité a pour objectif de vous faire travailler avec la structure de données de type pile.

Elle se déroule en 3 temps :

  • Élaboration de la classe Pile
  • Utilisation de cette classe pour modéliser un paquet de 52 cartes
  • Utilisation de cette classe pour établir un algorithme de recherche de chemin "de sortie" dans une d'un labyrinth

Vous pouvez vous référer au cours de Quentin Konieczko en pdf, en cas de difficulté avec la POO.

La classe Pile

Avec de définir la classe Pile, nous avons besoin de définir la classe Node qui sera un noeud (une case, un emplacement, ...) de notre pile. Voici ci-dessous la représentation des classes Node et Pile :

Représentation de la classe Node et Pile

La classe Node

La classe possède donc 2 attributs :

  • self.value : Une valeur (qui peut-être de n'importe quel type)
  • self.nxt : Le noeud suivant

Compléter le code suivant :

class Node():
    def __init__(self, valeur, suivant):
        # ----------
        self.value ...
        ...
        # ----------

La classe Pile

La classe Pile doit possèder un élément "courant" qui est l'élément sur le dessus de la pile. Dans le cas du schéma précédent, ce sera le noeud qui contient le 3 de carreau. Elle doit aussi posèder un entier n qui compte le nombre d'éléments dans la pile

Dans la définition d constructeur __init__, nous devons retrouver :

  • self.current : L'élément en haut de la pile, initialisé par défaut à None.
  • self.n : Nombre d'éléments dans la pile, initialisé par défaut à 0.

Compléter le code suivant :

class Pile():
    def __init__(self):
        # ----------
        ...
        # ----------

La classe Pile doit possèder les méthodes suivantes :

  • is_empty(self) : Pour vérifier que la pile est vide.
  • get_top(self) : Pour obtenir la valeur de l'élément courant.
  • add(self, valeur) : Empile un noeud à la pile.
  • depile(self) : Dépile l'élément courant de la pile.
  • vide_la_pile(self) : Vide la pile.

La pile est-elle vide ?

Il est pratique d'implanter une méthode permettant de savoir si la pile est vide.

  • is_empty(self) : Retourne True si self.current est à None (vous pouvez aussi vérifier la valeur de self.n)

Compléter le code suivant :

class Pile():
    # ....
    def is_empty(self):
        # ----------
        ...
        # ----------

On empile...

Pour empiler un élément dans la pile, il faut ajouter un Node qui deviendra le noeud courant de la pile et dont le suivant est l'ancien noeud courant.

  • add(self, valeur) : Empile un noeud à la pile. Ne pas oublier d'incrémenter self.n.

Compléter le code suivant :

class Pile():
    # ....
    def add(self,valeur):
        # ----------
        ...
        # ----------

... et on "dépile"

La méthode depile(self) doit retourner la valeur de l'élément courant et faire de l'élément suivant de d'élément courant, le nouvel élément courant.

  • depile(self) : Retourne la valeur de l'élément courant et le retire de la pile. Ne pas oublier de décrémenter self.n. Si la liste est vide, depile(self) retourne None.

Compléter le code suivant :

class Pile():
    # ....
    def depile(self):
        # ----------
        ...
        # ----------

Être au top !

La méthode get_top(self) doit retourner la valeur de l'élément courant si la liste n'est pas vide.

  • get_top(self) : Retourne la valeur de l'élément courant. Si la liste est vide, get_top(self) retourne None.

Compléter le code suivant :

class Pile():
    # ....
    def get_top(self):
        # ----------
        ...
        # ----------

Tout vider

La méthode vide_la_pile(self) permet de vider la pile.

  • vide_la_pile(self) : Depile les éléments tant que la pile n'est pas vide.

Compléter le code suivant :

class Pile():
    # ....
    def get_top(self):
        # ----------
        ...
        # ----------

Tests

A ce stade, vous pouvez tester toutes les méthodes de votre Pile :

>>> p=Pile()
>>> p.is_empty()
True
>>> p.add("Patate")
>>> p.is_empty()
False
>>> p.get_top()
'Patate'
>>> p.add(6)
>>> p.get_top()
6
>>> p.add(789)
>>> p.add(["a","b"]) 
>>> p.add((1,2,3,4,5,6,7)) 
>>> p.get_top()
(1, 2, 3, 4, 5, 6, 7)
>>> p.depile()
(1, 2, 3, 4, 5, 6, 7)
>>> p.depile()
['a', 'b']
>>> p.depile()
789
>>> p.depile()
6
>>> p.depile()
'Patate'
>>> p.depile()
>>> p.is_empty()
True

Un jeu de cartes

On peut utiliser une pile pour modéliser un jeu de cartes.

Un jeu de cartes

Pour cela, il faut :

  • Générer une liste jeu_trie de tuples (valeur,couleur) de toutes les cartes d'un jeu de 52 cartes.
  • Choisir au hasard un élément parmi jeu_trie.
  • Empiler cet élément dans une pile et le retirer de jeu_trie.
  • Recommencer jusqu'à ce que la liste soit vide.

Écrire une fonction DeckOfCards() qui retourne un jeu de cartes mélangé.

def DeckOfCards():
    jeu = Pile()

    jeu_trie = []
    for valeur in [2,3,4,5,6,7,8,9,10,"Valet","Dame","Roi","As"]:
        for couleur in ["Pique","Coeur","Trèfle","Carreau"]:
            # ...
    # ...
    return jeu

Remarque : list.pop(i) enlève de la liste l'élément situé à la position indiquée et le renvoie en valeur de retour. Si aucune position n'est spécifiée, a.pop() enlève et renvoie le dernier élément de la liste.

Blackjack

But du jeu : Après avoir reçu deux cartes, le joueur tire des cartes pour sapprocher de la valeur 21 sans la dépasser. Le but du joueur est de battre le croupier en obtenant un total de points supérieur à celui-ci ou en voyant ce dernier dépasser 21. Chaque joueur joue contre le croupier, qui représente la banque, ou le casino, et non contre les autres joueurs.

A ce stade, vous pouvez écrire une fonction qui tire 2 cartes dans un jeu de cartes et qui calcule la valeur de la main de Blackjack correspondant.

def main_bjack(jeu):
    # ...

La pile utilisé pour piocher les cartes étant la même pour chaque tirage, vous devriez toujours obtenir des cartes différentes.

>>> j=DeckOfCards()
>>> while(not j.is_empty()):
...     main_bjack(j)
...
('Dame', 'Carreau') (3, 'Trèfle') 13
13
(8, 'Carreau') (5, 'Carreau') 13
13
(6, 'Coeur') ('As', 'Pique') 17
17
...
...

Sortir d'un labyrinthe

On peut modéliser un labyrinthe par une chaîne de caractères de la manière suivante :

maze_str="""
################
#e             #
##### ###### ###
#  #   #     #s#
## ##### ### # #
#          #   #
################
"""
  • e : Entrée du labyrinthe
  • s : Sortie du labyrinthe
  • # : Mur
  • Une position dans le labyrinthe peut-être modèlisé par un tuple (ligne,colonne)

Pour pouvoir accéder à une case du labyrinthe de manière plus pratique : maze[ligne][colonne], vous pouvez transformer cette chaîne de caractères en listes :

maze = maze_str.split("\n")          # La première et dernière ligne sont vides (cela n'influence pas l'algorithme)
>>> maze[2][2]                       # Ligne n°2 , Colonne n°2 : position non explorée
' '
>>> maze[2][1]                       # Ligne n°2 , Colonne n°1 : entrée du labyrinthe
'e'
>>> maze[4][0]                       # Ligne n°4 , Colonne n°0 : un mur
'#'

Pour trouver la sortie du labyrinthe à l'aide d'une pile :

  • Empiler la position de départ sur la pile
  • Tant que la pile n'est pas vide :
    • Se rendre sur la position "du haut" de la pile
    • Marquer notre position comme explorée (X)
    • Empiler les positions adjacentes inexplorées (en haut, en bas, à gauche ,à droite)

Pour écrire l'algorithme permettant d'explorer un labyrinthe, je vous conseille d'écrire les fonctions suivantes :

  • aff(t) : Affichage du labyrinthe t, t étant une liste de chaîne de caractères représentant chacune une ligne du labyrinthe.
  • find_e(t) : Retourne la ligne et la colonne où se situe l'entrée dans t.
  • avance(t, p) : p étant la pile des positions inexplorées, cette fonction doit faire :
    • Avance sur la position inexplorée suivante.
    • Marque la position courante comme explorée.
    • Empile les positions adjacentes inexplorées dans p.
    • Si on a trouvé la sortie s, on vide la pile p.

Écrire les fonctions ci-dessus et tester les à l'aide de :

import time

...

# Tant que la pile n'est pas vide ... on avance
p = Pile()
p.add(find_e(maze))
while(not p.is_empty()):
    avance(maze,p)
    aff(maze)
    time.sleep(0.5)

Vous trouverez à la fin du fichier, des exmeples de labyrithes pour tester votre algorithme :

maze_str = """
######################################
#e         # #   #   #               #
######  ## # # # # #  # ##############
#      ##  # #   #   #  #        ##s #
######  ## # # ####   # # ## # #  ## #
#       #            #     #   ## #  #
# ############## # # ########  #  # ##
# #        #       # #     #   # ##  #
# ##### ############ # # #   ###    ##
#              #       # # #   ## # ##
######################################
"""
... etc ...

Bonus : tkinter

Réaliser l'affichage du labyrinthe à l'aide de la bibliothèque tkinter.

Vous pouvez consulter la documentation suivante : Tkinter pour ISN notament la partie sur Canvas :

from tkinter import *
root = Tk()
root.title = 'Titre fenêtre'

canevas = Canvas(root, width=400, height=400, background="#FFFFFF")
canevas.create_rectangle(10, 20, 40, 50, fill="#00FF00")
canevas.create_rectangle(40, 50, 100, 100, fill="#0000FF")
canevas.pack()

root.mainloop()

Remarque :

after(delai_ms, fonc=None, *args)

Demande à Tkinter dappeller la fonction de rappel fonc avec les arguments args après lécoulement du délai delai_ms donné en millisecondes. Votre fonction de rappel ne peut pas être appelée avant ce délai (même si son appel effectif peut le dépasser) et elle ne sera appelée quune fois.

Exemple d'utilisation :

from tkinter import *

root = Tk()

def task():
    print("hello")
    root.after(2000, task)  # reschedule event in 2 seconds

root.after(2000, task)
root.mainloop()

Remerciements

Merci à Quentin Konieczko pour son cours sur la POO.