Skip to content

Commit 4aa7f6a

Browse files
committed
Merge branch 'master' of github.com:zestedesavoir/zestedecode
2 parents fb01fa2 + 9865efd commit 4aa7f6a

24 files changed

Lines changed: 853 additions & 1424 deletions

ateliers/snake/README.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,40 @@ Dans le second sous-objectif, les participants sont invités à créer une zone
6464

6565
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.
6666

67-
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.
67+
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.
6868

69-
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.
69+
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.
7070

71-
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.
71+
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.
7272

7373
#### Troisième objectif
7474

7575
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.
7676

77-
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.
77+
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.
7878

7979
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.
8080

81-
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.
81+
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.
82+
83+
#### Quatrième objectif
84+
85+
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é :
86+
87+
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.
88+
89+
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`.
90+
91+
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.
92+
93+
#### Bonus
94+
95+
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.
96+
97+
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.
98+
99+
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`.
100+
101+
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.
102+
103+
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.

ateliers/snake/bibliotheque.py

Lines changed: 88 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
### Bibliothèque Snake ###
33
### (c) Zeste de Savoir (c) ###
44
### Licence GPL ###
5-
### Auteur : TAlone ###
5+
### Auteurs : TAlone, Situphen ###
66
########################################
77

88
# Import des bibliothèques pygame et dotmap (faire attention à les installer: pip install pygame dotmap)
99
import pygame
10+
import pygame.freetype
1011
from dotmap import DotMap
1112
# Import de la fonction tirant des nombres aléatoires
1213
from random import randint
@@ -29,7 +30,8 @@
2930
"FLECHE_DROITE": pygame.K_RIGHT,
3031
"FLECHE_GAUCHE": pygame.K_LEFT,
3132
"FLECHE_HAUT": pygame.K_UP,
32-
"FLECHE_BAS": pygame.K_DOWN
33+
"FLECHE_BAS": pygame.K_DOWN,
34+
"ESPACE": pygame.K_SPACE
3335
})
3436

3537
class Serpent:
@@ -42,6 +44,12 @@ class Serpent:
4244
"STOP": 4
4345
})
4446

47+
# Liste des directions de rotations possibles
48+
ROTATIONS = DotMap({
49+
"HORAIRE": 90,
50+
"ANTI_HORAIRE": 270
51+
})
52+
4553
# Liste des parties du serpent pour une plus grande aisance
4654
PARTIES = DotMap({
4755
"TETE": 0,
@@ -52,91 +60,106 @@ class Serpent:
5260
# initX, initY : positions initiales de la tête
5361
def __init__(self, initX=160, initY=64):
5462
# Positions (x, y) du serpent sur la fenêtre de jeu
55-
self.sX = [initX, initX - GRILLE, initX - 2 * GRILLE]
56-
self.sY = [initY, initY, initY]
63+
self.__sX = [initX, initX - GRILLE, initX - 2 * GRILLE]
64+
self.__sY = [initY, initY, initY]
5765
# Rotations des morceaux de serpent
58-
self.sR = [0, 0, 0]
66+
self.__sR = [0, 0, 0]
5967

6068
def __avancer_serpent(self):
6169
# Avancement du corps du serpent : chaque morceau prend la position du précédent
62-
for i in range(len(self.sX) - 1, 0, -1):
63-
self.sX[i] = self.sX[i - 1]
64-
self.sY[i] = self.sY[i - 1]
65-
self.sR[i] = self.sR[i - 1]
70+
for i in range(len(self.__sX) - 1, 0, -1):
71+
self.__sX[i] = self.__sX[i - 1]
72+
self.__sY[i] = self.__sY[i - 1]
73+
self.__sR[i] = self.__sR[i - 1]
74+
75+
# Forcer la queue à être orientée comme le corps juste avant
76+
self.__sR[len(self.__sX) - 1] = self.__sR[len(self.__sX) - 2]
6677

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

7182
# Puis mettre à jour la tête selon la direction souhaitée
7283
if direction == self.DIRECTIONS.DROITE:
73-
self.sX[0] += pas
84+
self.__sX[0] += pas
7485
elif direction == self.DIRECTIONS.GAUCHE:
75-
self.sX[0] -= pas
86+
self.__sX[0] -= pas
7687
elif direction == self.DIRECTIONS.BAS:
77-
self.sY[0] += pas
88+
self.__sY[0] += pas
7889
elif direction == self.DIRECTIONS.HAUT:
79-
self.sY[0] -= pas
90+
self.__sY[0] -= pas
8091

8192
# Les rotations sont plus simples grâce aux constantes
82-
self.sR[0] = 90 * direction
93+
self.__sR[0] = 90 * direction
8394

8495
def morceaux(self, longueur):
8596
# Vérifions que les participants ne fassent pas n'importe quoi
86-
if(longueur > len(self.sX)):
87-
longueur = len(self.sX)
97+
if(longueur > len(self.__sX)):
98+
longueur = self.taille
8899

89100
# Pour chaque partie du corps...
90101
for i in range(longueur, 0, -1):
91102
# ...on regarde d'abord le type...
92103
type = self.PARTIES.CORPS
93104
if i == 1: type = self.PARTIES.TETE
94-
elif i == len(self.sX): type = self.PARTIES.QUEUE
105+
elif i == len(self.__sX): type = self.PARTIES.QUEUE
106+
107+
# ...puis on déduit le sens de la rotation...
108+
direction_rotation = self.__sR[i - 1] - self.__sR[i - 2]
109+
110+
# Dans certains cas, la rotation ne tombe pas juste
111+
# il faut donc l'ajuster
112+
if direction_rotation < 0:
113+
direction_rotation += 360
95114

96115
# ...avant de donner les informations nécessaires.
97116
yield DotMap({
98-
"position": (self.sX[i - 1], self.sY[i - 1]),
99-
"rotation": self.sR[i - 1],
117+
"position": (self.__sX[i - 1], self.__sY[i - 1]),
118+
"rotation": self.__sR[i - 1],
119+
"direction_rotation": direction_rotation,
100120
"type": type
101121
})
102122

103123
# NOTE: la taille ne doit pas pouvoir être modifiée directement par l'utilisateur
104124
@property
105125
def taille(self):
106-
return len(self.sX)
126+
return len(self.__sX)
107127

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

112132
# On ajoute une composante de rotation similaire
113-
self.sR.append(direction * 90)
133+
self.__sR.append(direction * 90)
114134

115135
# En fonction de la direction, il faut ajouter la composante à différents endroits
116136
if direction == self.DIRECTIONS.DROITE:
117-
self.sX.append(self.sX[len(self.sX) - 1] - GRILLE)
118-
self.sY.append(self.sY[len(self.sY) - 1])
137+
self.__sX.append(self.__sX[len(self.__sX) - 1] - GRILLE)
138+
self.__sY.append(self.__sY[len(self.__sY) - 1])
119139
elif direction == self.DIRECTIONS.GAUCHE:
120-
self.sX.append(self.sX[len(self.sX) - 1] + GRILLE)
121-
self.sY.append(self.sY[len(self.sY) - 1])
140+
self.__sX.append(self.__sX[len(self.__sX) - 1] + GRILLE)
141+
self.__sY.append(self.__sY[len(self.__sY) - 1])
122142
elif direction == self.DIRECTIONS.BAS:
123-
self.sX.append(self.sX[len(self.sX) - 1])
124-
self.sY.append(self.sY[len(self.sY) - 1] - GRILLE)
143+
self.__sX.append(self.__sX[len(self.__sX) - 1])
144+
self.__sY.append(self.__sY[len(self.__sY) - 1] - GRILLE)
125145
elif direction == self.DIRECTIONS.HAUT:
126-
self.sX.append(self.sX[len(self.sX) - 1])
127-
self.sY.append(self.sY[len(self.sY) - 1] + GRILLE)
146+
self.__sX.append(self.__sX[len(self.__sX) - 1])
147+
self.__sY.append(self.__sY[len(self.__sY) - 1] + GRILLE)
128148

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

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

160+
# Initialisation du texte
161+
pygame.freetype.init()
162+
140163
# Déclarations des paramètres facultatifs (fonctions du jeu)
141164
if initialisation is None:
142165
def initialisation(jeu):
@@ -146,25 +169,33 @@ def initialisation(jeu):
146169
def boucle(jeu):
147170
return
148171

149-
# Ajout des variables internes
150-
self.images = { }
151-
self.ecran = pygame.display.set_mode((largeur, hauteur))
152-
self.horloge = pygame.time.Clock()
172+
# Ajout des dimensions (modifiables)
153173
self.largeur = largeur
154174
self.hauteur = hauteur
175+
176+
# Ajout des variables internes
177+
self.__images = { }
178+
self.__ecran = pygame.display.set_mode((largeur, hauteur))
179+
self.__horloge = pygame.time.Clock()
155180
self.__ouvert = True
156181
self.__initialisation = initialisation
157182
self.__boucle = boucle
158183

184+
# Initialisation des polices
185+
# NOTE: cette fonction peut être de nouveau appelée ultérieurement pour changer les paramètres
186+
self.init_text()
187+
159188
# Un peu d'esthétique
160-
# pygame.display.set_icon(apple)
161189
pygame.display.set_caption("Snake")
162190

163191
# Lancement de l'initialisation
164192
self.__initialisation(self)
165193
# Lancement de la boucle infinie principale
166194
self.boucle()
167195

196+
def init_text(self, font="sans-serif", size=32):
197+
self.__police = pygame.freetype.SysFont(font, size)
198+
168199
def boucle(self):
169200
# Lancement de la boucle tant que le jeu est ouvert
170201
while self.__ouvert:
@@ -181,39 +212,43 @@ def boucle(self):
181212
# Lancement de la boucle utilisateur
182213
self.__boucle(self)
183214

184-
# Terminer le dessin du jeu
185-
self.__rafraichir_ecran()
215+
if self.__ouvert:
216+
# Terminer le dessin du jeu
217+
self.__rafraichir_ecran()
186218

187219
# Chargement d'un asset dans PyGame
188220
def ajouter_image(self, name, path):
189-
self.images[name] = pygame.image.load(path)
221+
self.__images[name] = pygame.image.load(path)
222+
223+
def ajouter_texte(self, name, text):
224+
self.__images[name] = self.__police.render(text, (0, 0, 0))[0]
190225

191226
def effacer_ecran(self):
192227
# Remplit l'écran de jaune
193-
self.ecran.fill((255, 255, 0))
228+
self.__ecran.fill((255, 255, 0))
194229

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

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

207242
def __rafraichir_ecran(self):
208243
# Rafraichissement de l'écran
209244
pygame.display.flip()
210245
# Délai avant la prochaine frame (6 FPS)
211-
self.horloge.tick(6)
246+
self.__horloge.tick(6)
212247

213248
def quitter(self):
214249
self.__ouvert = False
215250

216-
def position_aleatoire(self):
251+
def __position_aleatoire(self):
217252
# Donne une position sur la zone de jeu
218253
return (randint(0, ceil(self.largeur / GRILLE)) * GRILLE, randint(0, ceil(self.hauteur / GRILLE)) * GRILLE)
219254

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

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

228263
# Ne pas mettre la pomme sur le serpent
229-
for morceau in self.snake.morceaux():
264+
for morceau in self.serpent.morceaux(self.serpent.taille):
230265
if self.collision(pomme, morceau.position):
231266
# Tant que la pomme est sur le joueur, on la change de place
232-
pomme = self.position_aleatoire()
267+
pomme = self.__position_aleatoire()
233268

234269
# Retourne la position trouvée
235-
return pomme
270+
return DotMap({
271+
"position": pomme,
272+
"rotation": 0
273+
})
236274

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

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

0 commit comments

Comments
 (0)