Skip to content

Commit

Permalink
Merge branch 'master' of github.com:zestedesavoir/zestedecode
Browse files Browse the repository at this point in the history
  • Loading branch information
AmauryCarrade committed Apr 23, 2019
2 parents fb01fa2 + 9865efd commit 4aa7f6a
Show file tree
Hide file tree
Showing 24 changed files with 853 additions and 1,424 deletions.
32 changes: 27 additions & 5 deletions ateliers/snake/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,40 @@ Dans le second sous-objectif, les participants sont invités à créer une zone

Cet objectif présente le fonctionnement des boucles, et initie les participants à la gestion de tableaux. Il est très peu guidé, et présente comme objectif de faire lire la documentation aux participants. La réalisation des trois sous-objectifs peut être longue et demander beaucoup de patience aux organisateurs, afin de poser les bases. Les rudiments de la programmation vu précédemment (structures conditionnelles et variables) seront aussi réutilisés afin de les consolider.

Le premier sous-objectif consiste à afficher les bordures de la zone en forme de cactus ; en cela, il est très simple et vise surtout à comprendre l’intérêt des boucles. Il est conseillé de montrer aux participants comment remplir la zone de jeu de cactus avant se limiter à en afficher seulement sur les bords. La phase de recherche de fonctions est très importante à ce stade car deux fonctions utiles : `screenIterator` et `isSide` seront nécessaires pour finaliser cet objectif.
Le premier sous-objectif consiste à afficher les bordures de la zone en forme de cactus ; en cela, il est très simple et vise surtout à comprendre l’intérêt des boucles. Il est conseillé de montrer aux participants comment remplir la zone de jeu de cactus avant se limiter à en afficher seulement sur les bords. La phase de recherche de fonctions est très importante à ce stade car deux fonctions utiles : `grille` et `est_un_bord` seront nécessaires pour finaliser cet objectif.

Le second sous-objectif voit simplement un autre type de boucle qui itère sur un nombre donné de parties du serpent : `partsIterator`. Une compréhension au moins grossière du fonctionnement des tableaux est nécessaire afin de passer les arguments, mais cet objectif devrait être simple et court.
Le second sous-objectif voit simplement un autre type de boucle qui itère sur un nombre donné de parties du serpent : `serpent.morceaux`. Une compréhension au moins grossière du fonctionnement des tableaux est nécessaire afin de passer les arguments, mais cet objectif devrait être simple et court.

Le dernier sous-objectif révise les structures conditionnelles en affichant les différentes parties du serpent : queue, tête et corps. Une partie encore une fois simple visant à compléter le second sous-objectif. Les participants veilleront à ne pas oublier de déclarer les sprites avec la fonction `addAsset` avant de les utiliser ; en cas d’oubli, ils seront invités à tenter de comprendre les erreurs Python.
Le dernier sous-objectif révise les structures conditionnelles en affichant les différentes parties du serpent : queue, tête et corps. Une partie encore une fois simple visant à compléter le second sous-objectif. Les participants veilleront à ne pas oublier de déclarer les sprites avec la fonction `ajouter_image` avant de les utiliser ; en cas d’oubli, ils seront invités à tenter de comprendre les erreurs Python.

#### Troisième objectif

Le troisième objectif complexifie beaucoup le code contrairement aux précédents ; il ajoute en effet une grosse partie de gestion des événements afin de vérifier l’appui des différentes touches de mouvement (les flèches directionnelles). La recherche dans la documentation sera très importante, mais aucune notion nouvelle n’est abordée à partir d’ici. Les participants qui s’arrêteront à cet objectif auront les bases nécessaires en programmation, et ce afin de créer des petits programmes simples.

Ici, le premier sous-objectif consiste à poser les bases de la direction du serpent, en particulier la déclaration d’une variable de direction ainsi que le déplacement effectif du serpent à chaque tour de boucle. Parmi les problèmes rencontrés, il est certain que figureront l’oubli de la portée globale de la variable de direction ainsi que l’oubli de l’appel à la fonction `snake.move`, qui causera le serpent à… ne pas bouger du tout, malgré un code fonctionnel.
Ici, le premier sous-objectif consiste à poser les bases de la direction du serpent, en particulier la déclaration d’une variable de direction ainsi que le déplacement effectif du serpent à chaque tour de boucle. Parmi les problèmes rencontrés, il est certain que figureront l’oubli de la portée globale de la variable de direction ainsi que l’oubli de l’appel à la fonction `serpent.deplacer`, qui causera le serpent à… ne pas bouger du tout, malgré un code fonctionnel.

Le second sous-objectif est une extension simple du premier qui consiste à gérer les quatre directions en se renseignant dans la documentation ainsi qu’à stocker la direction souhaitée dans la variable du jeu pour pouvoir l’utiliser par la suite.

Les objectifs suivants étant prévus pour les plus avancés, ils seront moins documentés pour les organisateurs, sous forme plus synthétique. Après la fin des objectifs, des pistes seront proposées aux participants qui seront libres de les implémenter ou non.
L’objectif suivant étant prévu pour les plus avancés, il est moins documenté pour les organisateurs, sous forme plus synthétique. Après la fin des objectifs, des pistes seront proposées aux participants qui seront libres de les implémenter ou non.

#### Quatrième objectif

Cet objectif complet revient sur tout ce qui a été vu avant, et permet au participant d’acquérir une vue globale sur son code. En plus de celà, l’objectif finalise en quelque sorte le jeu en y ajoutant toutes les collisions nécessaires, ainsi que la pomme (enfin !). Comme indiqué ci-avant, voici une description synthétique de ce qui est proposé :

Le premier sous-objectif consiste simplement en l’ajout de la pomme et son dessin en fin de boucle. En outre, les participants auront besoin de la fonction `position_aleatoire_pomme`, qu’ils seront fortement invités à trouver dans la documentation.

Le second sous-objectif a pour objet la mise en place des collisions entre le serpent et la pomme. La fonction `collision` sera découverte. Pour les plus curieux, les objets pourront être abordés par la nécessité d’utiliser `pomme.position`.

Les troisième et quatrième sous-objectifs sont aussi des objectifs de collisions, simple si les participants ont compris le premier module de collisions.

#### Bonus

Une fois le quatrième objectif terminé, les participants pourront, selon leur choix, soit développer une fonctionnalité qu’ils souhaiteraient voir dans le jeu, soit tenter de concevoir un des bonus proposés. L’objectif des bonus n’est pas de les terminer tous, mais plutôt de revenir sur son code afin d’en acquérir une vision plus globale, ainsi que de revoir les acquis du jour. Il peut être bon pour les participants de réaliser ces bonus quelques jours après l’atelier, afin d’assimiler correctement et même d’approfondir les notions étudiées.

Le premier bonus, plutôt simple, résoud un problème présent dans les objectifs précédents : le serpent peut se mordre en allant en arrière. Or, dans la plupart des jeux Snake, il n’est pas possible d’aller vers l’arrière ; les participants seront donc invités à résoudre ce problème pour se faire la main.

Le second objectif bonus ajoute un compteur de points (qui est simplement la taille du serpent moins la taille initiale). L’idée est ici de se familiariser avec les fonctions de texte, particulièrement la fonction `ajouter_texte`, qui fonctionne de la même façon que `ajouter_image`.

Le troisième bonus, un peu plus complexe, vise à utiliser ces fonctions de texte afin d’afficher au joueur son score en fin de partie et lui permettre de rejouer.

Enfin, les participants les plus doués pourront proposer une solution pour gérer les rotations du serpent proprement en utilisant l’image `coin.png` mise à leur disposition. Cet objectif est complexe car il demande une bonne compréhension du fonctionnement des objets, ainsi qu’une idée (au moins vague), de la façon dont est géré le dessin du serpent en interne.
139 changes: 88 additions & 51 deletions ateliers/snake/bibliotheque.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
### Bibliothèque Snake ###
### (c) Zeste de Savoir (c) ###
### Licence GPL ###
### Auteur : TAlone ###
### Auteurs : TAlone, Situphen ###
########################################

# Import des bibliothèques pygame et dotmap (faire attention à les installer: pip install pygame dotmap)
import pygame
import pygame.freetype
from dotmap import DotMap
# Import de la fonction tirant des nombres aléatoires
from random import randint
Expand All @@ -29,7 +30,8 @@
"FLECHE_DROITE": pygame.K_RIGHT,
"FLECHE_GAUCHE": pygame.K_LEFT,
"FLECHE_HAUT": pygame.K_UP,
"FLECHE_BAS": pygame.K_DOWN
"FLECHE_BAS": pygame.K_DOWN,
"ESPACE": pygame.K_SPACE
})

class Serpent:
Expand All @@ -42,6 +44,12 @@ class Serpent:
"STOP": 4
})

# Liste des directions de rotations possibles
ROTATIONS = DotMap({
"HORAIRE": 90,
"ANTI_HORAIRE": 270
})

# Liste des parties du serpent pour une plus grande aisance
PARTIES = DotMap({
"TETE": 0,
Expand All @@ -52,91 +60,106 @@ class Serpent:
# initX, initY : positions initiales de la tête
def __init__(self, initX=160, initY=64):
# Positions (x, y) du serpent sur la fenêtre de jeu
self.sX = [initX, initX - GRILLE, initX - 2 * GRILLE]
self.sY = [initY, initY, initY]
self.__sX = [initX, initX - GRILLE, initX - 2 * GRILLE]
self.__sY = [initY, initY, initY]
# Rotations des morceaux de serpent
self.sR = [0, 0, 0]
self.__sR = [0, 0, 0]

def __avancer_serpent(self):
# Avancement du corps du serpent : chaque morceau prend la position du précédent
for i in range(len(self.sX) - 1, 0, -1):
self.sX[i] = self.sX[i - 1]
self.sY[i] = self.sY[i - 1]
self.sR[i] = self.sR[i - 1]
for i in range(len(self.__sX) - 1, 0, -1):
self.__sX[i] = self.__sX[i - 1]
self.__sY[i] = self.__sY[i - 1]
self.__sR[i] = self.__sR[i - 1]

# Forcer la queue à être orientée comme le corps juste avant
self.__sR[len(self.__sX) - 1] = self.__sR[len(self.__sX) - 2]

def deplacer(self, direction, pas=GRILLE):
# D'abord, bouger tout le corps
if direction != self.DIRECTIONS.STOP: self.__avancer_serpent()

# Puis mettre à jour la tête selon la direction souhaitée
if direction == self.DIRECTIONS.DROITE:
self.sX[0] += pas
self.__sX[0] += pas
elif direction == self.DIRECTIONS.GAUCHE:
self.sX[0] -= pas
self.__sX[0] -= pas
elif direction == self.DIRECTIONS.BAS:
self.sY[0] += pas
self.__sY[0] += pas
elif direction == self.DIRECTIONS.HAUT:
self.sY[0] -= pas
self.__sY[0] -= pas

# Les rotations sont plus simples grâce aux constantes
self.sR[0] = 90 * direction
self.__sR[0] = 90 * direction

def morceaux(self, longueur):
# Vérifions que les participants ne fassent pas n'importe quoi
if(longueur > len(self.sX)):
longueur = len(self.sX)
if(longueur > len(self.__sX)):
longueur = self.taille

# Pour chaque partie du corps...
for i in range(longueur, 0, -1):
# ...on regarde d'abord le type...
type = self.PARTIES.CORPS
if i == 1: type = self.PARTIES.TETE
elif i == len(self.sX): type = self.PARTIES.QUEUE
elif i == len(self.__sX): type = self.PARTIES.QUEUE

# ...puis on déduit le sens de la rotation...
direction_rotation = self.__sR[i - 1] - self.__sR[i - 2]

# Dans certains cas, la rotation ne tombe pas juste
# il faut donc l'ajuster
if direction_rotation < 0:
direction_rotation += 360

# ...avant de donner les informations nécessaires.
yield DotMap({
"position": (self.sX[i - 1], self.sY[i - 1]),
"rotation": self.sR[i - 1],
"position": (self.__sX[i - 1], self.__sY[i - 1]),
"rotation": self.__sR[i - 1],
"direction_rotation": direction_rotation,
"type": type
})

# NOTE: la taille ne doit pas pouvoir être modifiée directement par l'utilisateur
@property
def taille(self):
return len(self.sX)
return len(self.__sX)

def grandir(self):
# On regarde d'abord la direction de la dernière partie du serpent
direction = self.sR[len(self.sR) - 1] / 90
direction = self.__sR[len(self.__sR) - 1] / 90

# On ajoute une composante de rotation similaire
self.sR.append(direction * 90)
self.__sR.append(direction * 90)

# En fonction de la direction, il faut ajouter la composante à différents endroits
if direction == self.DIRECTIONS.DROITE:
self.sX.append(self.sX[len(self.sX) - 1] - GRILLE)
self.sY.append(self.sY[len(self.sY) - 1])
self.__sX.append(self.__sX[len(self.__sX) - 1] - GRILLE)
self.__sY.append(self.__sY[len(self.__sY) - 1])
elif direction == self.DIRECTIONS.GAUCHE:
self.sX.append(self.sX[len(self.sX) - 1] + GRILLE)
self.sY.append(self.sY[len(self.sY) - 1])
self.__sX.append(self.__sX[len(self.__sX) - 1] + GRILLE)
self.__sY.append(self.__sY[len(self.__sY) - 1])
elif direction == self.DIRECTIONS.BAS:
self.sX.append(self.sX[len(self.sX) - 1])
self.sY.append(self.sY[len(self.sY) - 1] - GRILLE)
self.__sX.append(self.__sX[len(self.__sX) - 1])
self.__sY.append(self.__sY[len(self.__sY) - 1] - GRILLE)
elif direction == self.DIRECTIONS.HAUT:
self.sX.append(self.sX[len(self.sX) - 1])
self.sY.append(self.sY[len(self.sY) - 1] + GRILLE)
self.__sX.append(self.__sX[len(self.__sX) - 1])
self.__sY.append(self.__sY[len(self.__sY) - 1] + GRILLE)

# NOTE: la position de la tete ne doit pas pouvoir être modifiée directement par l'utilisateur
@property
def position_tete(self):
# Donne la position de la tête du serpent
return (self.sX[0], self.sY[0])
return (self.__sX[0], self.__sY[0])

class Jeu:
def __init__(self, initialisation=None, boucle=None, largeur=LARGEUR, hauteur=HAUTEUR):
# Initialisation de PyGame
pygame.init()

# Initialisation du texte
pygame.freetype.init()

# Déclarations des paramètres facultatifs (fonctions du jeu)
if initialisation is None:
def initialisation(jeu):
Expand All @@ -146,25 +169,33 @@ def initialisation(jeu):
def boucle(jeu):
return

# Ajout des variables internes
self.images = { }
self.ecran = pygame.display.set_mode((largeur, hauteur))
self.horloge = pygame.time.Clock()
# Ajout des dimensions (modifiables)
self.largeur = largeur
self.hauteur = hauteur

# Ajout des variables internes
self.__images = { }
self.__ecran = pygame.display.set_mode((largeur, hauteur))
self.__horloge = pygame.time.Clock()
self.__ouvert = True
self.__initialisation = initialisation
self.__boucle = boucle

# Initialisation des polices
# NOTE: cette fonction peut être de nouveau appelée ultérieurement pour changer les paramètres
self.init_text()

# Un peu d'esthétique
# pygame.display.set_icon(apple)
pygame.display.set_caption("Snake")

# Lancement de l'initialisation
self.__initialisation(self)
# Lancement de la boucle infinie principale
self.boucle()

def init_text(self, font="sans-serif", size=32):
self.__police = pygame.freetype.SysFont(font, size)

def boucle(self):
# Lancement de la boucle tant que le jeu est ouvert
while self.__ouvert:
Expand All @@ -181,39 +212,43 @@ def boucle(self):
# Lancement de la boucle utilisateur
self.__boucle(self)

# Terminer le dessin du jeu
self.__rafraichir_ecran()
if self.__ouvert:
# Terminer le dessin du jeu
self.__rafraichir_ecran()

# Chargement d'un asset dans PyGame
def ajouter_image(self, name, path):
self.images[name] = pygame.image.load(path)
self.__images[name] = pygame.image.load(path)

def ajouter_texte(self, name, text):
self.__images[name] = self.__police.render(text, (0, 0, 0))[0]

def effacer_ecran(self):
# Remplit l'écran de jaune
self.ecran.fill((255, 255, 0))
self.__ecran.fill((255, 255, 0))

# Si un fond est défini, remplis l'écran avec ce fond
if self.images["fond"]:
if self.__images["fond"]:
for x in range(0, ceil(self.largeur / GRILLE)):
for y in range(0, ceil(self.hauteur / GRILLE)):
self.ecran.blit(self.images["fond"], (x * GRILLE, y * GRILLE))
self.__ecran.blit(self.__images["fond"], (x * GRILLE, y * GRILLE))

def dessiner(self, image, parametres):
# Effectue une rotation (éventuelle) de la tile
sprite = pygame.transform.rotate(self.images[image], parametres.get("rotation", 0))
sprite = pygame.transform.rotate(self.__images[image], parametres.get("rotation", 0))
# Dessin simple de la sprite à l'écran
self.ecran.blit(sprite, parametres.get("position", 0))
self.__ecran.blit(sprite, parametres.get("position", 0))

def __rafraichir_ecran(self):
# Rafraichissement de l'écran
pygame.display.flip()
# Délai avant la prochaine frame (6 FPS)
self.horloge.tick(6)
self.__horloge.tick(6)

def quitter(self):
self.__ouvert = False

def position_aleatoire(self):
def __position_aleatoire(self):
# Donne une position sur la zone de jeu
return (randint(0, ceil(self.largeur / GRILLE)) * GRILLE, randint(0, ceil(self.hauteur / GRILLE)) * GRILLE)

Expand All @@ -222,23 +257,25 @@ def position_aleatoire_pomme(self):
pomme = self.__position_aleatoire()

# Tant que la pomme est hors-zone, on la change de place
while self.est_sur_un_bord(pomme):
while self.est_un_bord(pomme):
pomme = self.__position_aleatoire()

# Ne pas mettre la pomme sur le serpent
for morceau in self.snake.morceaux():
for morceau in self.serpent.morceaux(self.serpent.taille):
if self.collision(pomme, morceau.position):
# Tant que la pomme est sur le joueur, on la change de place
pomme = self.position_aleatoire()
pomme = self.__position_aleatoire()

# Retourne la position trouvée
return pomme
return DotMap({
"position": pomme,
"rotation": 0
})

def collision(self, p1, p2):
# cf. https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
return (p1[0] < p2[0] + GRILLE) and (p1[0] + GRILLE > p2[0]) and (p1[1] < p2[1] + GRILLE) and (p1[1] + GRILLE > p2[1])

# TODO: Renommer le nom de la fonction ou ajouter un décorateur pour l'utiliser comme une variable
def grille(self):
# Renvoie un itérateur de chaque x et y disponible sur la grille
for x in range(0, ceil(self.largeur / GRILLE)):
Expand Down
Loading

0 comments on commit 4aa7f6a

Please sign in to comment.