|
|
|
'''
|
|
|
|
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()
|