diff --git a/SpaceInvader.py b/SpaceInvader.py index abdab6c..b2b6a1d 100644 --- a/SpaceInvader.py +++ b/SpaceInvader.py @@ -1,306 +1,319 @@ -''' -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.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 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, 8, 8) - # (..., 0, 0, 0, 8, 8) car Image 0 à partir de (0;0) de taille 8*8 - - -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.y = self.y + 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.y > 128 - - 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 + 8 and self.y <= v_y + 8 and self.x + 8 >= v_x and self.y + 8 >= v_y - - def afficher(self:'Ennemi') -> None: - """Affiche l'ennemi""" - pyxel.blt(self.x, self.y, 0, 0, 8, 8, 8) - # (..., 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_y(-1) - - 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(random.randint(0, 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() +''' +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, 8, 8) + # (..., 0, 0, 0, 8, 8) car Image 0 à partir de (0;0) de taille 8*8 + + 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 + 8 and self.y <= v_y + 8 and self.x + 8 >= v_x and self.y + 8 >= v_y + + def afficher(self:'Ennemi') -> None: + """Affiche l'ennemi""" + pyxel.blt(self.x, self.y, 0, 0, 8, 8, 8) + # (..., 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()