''' Programme pyxel inspiré d'un tutoriel en ligne de "la Nuit du Code" https://nuit-du-code.forge.apps.education.fr/DOCUMENTATION/PYTHON/01-presentation/ https://www.cahiernum.net/KV8H5B Licence GNU (https://github.com/nuitducode/DOCUMENTATION/blob/main/LICENSE) Module basé sur une architecture MVC (modèle-vue-controleur) globale et également intégrée aux objets. ''' # Importation import pyxel import random # Constantes COULEUR_VAISSEAU = 1 COULEUR_TIR = 10 COULEUR_ENNEMI = 8 # Déclaration des classes class Vaisseau: """Classe intégrant la gestion du Modèle et de la Vue relative au vaisseau du joueur.""" def __init__(self, couleur:int=COULEUR_VAISSEAU) -> None: self.x = 60 # coordonnée x du coin haut à gauche du carré self.y = 60 # coordonnée y du coin haut à gauche du carré self.jump = None self.couleur = couleur # couleur du vaisseau à l'écran def set_x(self:'Vaisseau', dx:int) -> None: """Déplace le vaisseau à gauche si dx positif, à droite si négatif""" self.x = self.x + dx if self.x < 0: self.x = 0 elif self.x >= 120: self.x = 120 def set_y(self:'Vaisseau', dy:int) -> None: """Déplace le vaisseau en bas si dy positif, en haut si négatif""" self.y = self.y + dy if self.y < 0: self.y = 0 elif self.y >= 120: self.y = 120 def set_jump(self): self.jump = 10 def get_coord(self:'Vaisseau') -> tuple[int, int]: """Renvoie le couple (x, y) qui contient les coordonnées (du coin haut gauche) du vaisseau""" return (self.x, self.y) def afficher(self:'Vaisseau') -> None: """Affiche le vaisseau""" pyxel.blt(self.x, self.y, 0, 0, 0, 20, 14) # (..., 0, 0, 0, 8, 8) car Image 0 à partir de (0;0) de taille 20*14 def move(self): if self.jump!=None: self.jump = self.jump - 1 if self.jump > 0: self.y = self.y - 1 if self.jump < 0: self.y = self.y + 1 if self.jump == -100: self.jump = None class Joueur: """Classe intégrant la gestion du Modèle relative au joueur.""" def __init__(self, vaisseau:'Vaisseau', vies:int) -> None: self.vies = vies # A 0, le joueur a perdu self.vaisseau = vaisseau # L'instance de Vaisseau du joueur def est_vivant(self:'Joueur') -> bool: """Prédicat qui renvoie vrai si le joueur a encore des vies""" return self.vies > 0 def get_vies(self:'Joueur') -> int: """Renvoie le nombre de vies du joueur""" return self.vies def perd_une_vie(self:'Joueur') -> None: """Fait perdre une vie au joueur mais en imposant un minimum de 0""" self.vies = self.vies - 1 if self.vies < 0: self.vies = 0 class Tir: """Classe intégrant la gestion du Modèle et de la Vue relative au vaisseau des tirs.""" def __init__(self, xv:int, yv:int, couleur:int=COULEUR_TIR) -> None: self.x = xv + 4 self.y = yv - 4 self.couleur = couleur def deplacement(self:'Tir') -> None: """Déplace le tir d'un pixel vers le haut""" self.y = self.y - 1 def est_hors_ecran(self:'Tir') -> bool: """Prédicat qui renvoie True si le tir est sorti de l'écran par le haut""" return self.y < -8 def get_coord(self:'Tir') -> tuple[int, int]: """Renvoie le couple (x,y) des coordonnées (du coin haut gauche) du tir""" return (self.x, self.y) def afficher(self:'Tir') -> None: pyxel.blt(self.x, self.y, 0, 12, 0, 1, 8) # (..., 0, 12, 0, 1, 8) car Image 0 à partir de (12;0) de taille 1*8 class Ennemi: """Classe intégrant la gestion du Modèle et de la Vue relative au vaisseau des ennemis.""" def __init__(self, x:int, y:int, couleur:int=COULEUR_ENNEMI) -> None: self.x = x self.y = y self.couleur = couleur def deplacement(self:'Ennemi') -> None: """Déplace l'ennemi d'un pixel vers le bas""" self.x = self.x - 1 def get_coord(self:'Ennemi') -> tuple[int, int]: """Renvoie le couple (x,y) des coordonnées (du coin haut gauche) de l'ennemi""" return (self.x, self.y) def est_hors_ecran(self:'Ennemi') -> bool: """Prédicat qui renvoie True si l'ennemi est sorti de l'écran par le bas""" return self.x < 0 def est_touche_par(self:'Ennemi', tir:'Tir') -> bool: """Prédicat qui renvoie True si l'ennemi est en contact avec le tir""" tir_x, tir_y = tir.get_coord() # les coordonnées du tir, objet d'une autre classe return tir_x >= self.x and tir_x < (self.x + 8) and tir_y < (self.y + 8) # # ou cette version, plus compréhensible car étape par étape if tir_x >= self.x: # si le x du tir dépasse le bord gauche du monstre if tir_x < (self.x + 8): # si le x du tir ne dépasse pas le bord droite du monstre # si on arrive ici, c'est que l'abscisse du tir est compatible avec une touche if tir_y < (self.y + 8): # si le coin haut du tir est plus petit que le coin bas du monstre return True return False def touche_vaisseau(self:'Ennemi', v:'Vaisseau') -> bool: """Prédicat qui renvoie True si l'ennemi est en contact avec le vaisseau du joueur""" v_x, v_y = v.get_coord() # on récupère les coordonnées d'un objet d'une autre classe return (self.x <= v_x + 16 and self.y <= v_y + 46 and self.x + 16 >= v_x and self.y + 46 >= v_y , self.x <= v_x + 16 and self.y <= v_y + 82 and self.x + 16 >= v_x and self.y + 82 >= v_y) def afficher(self:'Ennemi') -> None: """Affiche l'ennemi""" pyxel.blt(self.x, self.y, 0, 24, 0, 16, 46) pyxel.blt(self.x, self.y, 0, 24, 0, 16, 82) # (..., 0, 0, 8, 8, 8) car Image 0 à partir de (0;8) de taille 8*8 class Explosion: """Classe intégrant la gestion des explosions.""" def __init__(self, ennemi:'Ennemi') -> None: """On génère l'explosion du monstre en (x,y)""" xe, ye = ennemi.get_coord() self.x = xe + 4 # car self.x est le centre du cercle, pas son bord gauche self.y = ye + 4 # car self.y est le centre du cerlce, pas son bord haut self.force = 0 self.couleur = 8 + self.force % 3 self.rayon = 2 * (self.force // 4) def propager(self:'Explosion') -> None: """Propage l'explosion en augmentant son rayon""" self.force = self.force + 1 # on augmente la force self.couleur = 8 + self.force % 3 # on calcule la nouvelle couleur self.rayon = 2 * (self.force // 4) # on calcule le rayon de l'explosion def est_au_maximum(self:'Explosion') -> None: """Prédicat qui renvoie True si l'explosion a atteint son étendue maximale""" return self.force >= 12 def afficher(self:'Explosion') -> None: """Affiche l'explosion""" pyxel.circb(self.x, self.y, self.rayon, self.couleur) class Jeu: """Classe intégrant la gestion du jeu.""" def __init__(self) -> None: # Création de la fenêtre graphique pyxel.init(128, 128, title="Nuit du c0de") pyxel.load("space.pyxres") # Initialisation des données du jeu self.vaisseau = Vaisseau() self.joueur = Joueur(self.vaisseau, 4) self.tirs = [] # Tableau des tirs self.ennemis = [] # Tableau des ennemis présents self.explosions = [] # Tableau des explosions # Lancement de l'alternance 30x par seconde entre controleur et vue pyxel.run(self.controler, self.afficher) def controler(self:'Jeu') -> None: """déplacement avec les touches de directions""" self.se_deplacer() self.tirer() self.deplacer_tirs() self.ajouter_nouvel_ennemi() self.deplacer_ennemis() self.supprimer_ennemis_touches() self.modifier_explosions() def se_deplacer(self:'Jeu') -> None: """Contrôle les touches de déplacement et lance le déplacement au besoin""" if pyxel.btn(pyxel.KEY_RIGHT): self.vaisseau.set_x(1) if pyxel.btn(pyxel.KEY_LEFT): self.vaisseau.set_x(-1) if pyxel.btn(pyxel.KEY_DOWN): self.vaisseau.set_y(1) if pyxel.btn(pyxel.KEY_UP): self.vaisseau.set_jump() self.vaisseau.move() def tirer(self:'Jeu') -> None: """Contrôle la touche de création d'un tir et lance la création au besoin""" if pyxel.btnr(pyxel.KEY_SPACE): self.ajouter_un_tir() def ajouter_un_tir(self:'Jeu') -> None: """Ajoute un nouveau tir dans le jeu""" xv, yv = self.vaisseau.get_coord() nouveau_tir = Tir(xv, yv) self.tirs.append(nouveau_tir) def deplacer_tirs(self) -> None: """Contrôle le déplacement des tirs et leur suppression quand ils sortent du cadre""" for tir in list(self.tirs): # list() pour travailler sur une copie et éviter le problème de décalage des cases tir.deplacement() if tir.est_hors_ecran(): self.tirs.remove(tir) def ajouter_nouvel_ennemi(self:'Jeu') -> None: """Création aléatoire des ennemis""" if (pyxel.frame_count % 30 == 0): # 30 images / s donc un ennemi par seconde nouvel_ennemi = Ennemi(120, 0) self.ennemis.append(nouvel_ennemi) def deplacer_ennemis(self:'Jeu') -> None: """Déplace les ennemis et les supprime quand ils sortent du cadre ou sont touchés""" for ennemi in list(self.ennemis): # list() pour travailler sur une copie et éviter le problème de décalage de cases ennemi.deplacement() if ennemi.est_hors_ecran(): self.ennemis.remove(ennemi) elif ennemi.touche_vaisseau(self.vaisseau): self.ennemis.remove(ennemi) self.joueur.perd_une_vie() def supprimer_ennemis_touches(self:'Jeu') -> None: """Supprime l'ennemi et le tir au contact""" for ennemi in list(self.ennemis): # list() pour obtenir une copie des ennemis for tir in list(self.tirs): # list() pour obtenir une copie des ennemis if ennemi.est_touche_par(tir): self.ajouter_explosion(ennemi) self.ennemis.remove(ennemi) self.tirs.remove(tir) def ajouter_explosion(self:'Jeu', ennemi:'Ennemi') -> None: '''Ajoute dans les données une explosion naissante suite à la destruction de l'ennemi''' explosion = Explosion(ennemi) self.explosions.append(explosion) def modifier_explosions(self:'Jeu') -> None: """Modification des données des explosions""" for explosion in list(self.explosions): # list() pour obtenir une copie temporaire des explosions explosion.propager() if explosion.est_au_maximum(): self.explosions.remove(explosion) def afficher(self:'Jeu') -> None: """création et positionnement des objets (30 fois par seconde)""" pyxel.cls(0) # efface le contenu de la fenetre if self.joueur.est_vivant(): # si le joueur possède encore des vies self.afficher_vies() self.vaisseau.afficher() for tir in self.tirs: tir.afficher() for ennemi in self.ennemis: ennemi.afficher() for explosion in self.explosions : explosion.afficher() else: # sinon: GAME OVER self.afficher_game_over() def afficher_vies(self:'Jeu') -> None: """Affiche le nombre de vies du joueur""" pyxel.text(5, 5, f"VIES: {self.joueur.get_vies()}", 7) def afficher_game_over(self:'Jeu') -> None: """Affiche que le jeu est fini""" pyxel.text(50, 64, 'GAME OVER', 7) application = Jeu()