Aller au contenu

Menu de sélection des niveaux

Pour que notre jeu soit plus complet, on veut proposer une selection de niveaux, que le joueur peut choisir à partir d'un menu.

Principe général

On veut donner à notre jeu le comportement suivant :

Le joueur lance le jeu.

Une liste des niveaux apparaît à l'écran.

Le joueur fait défiler les niveaux en utilisant les flèches haut et bas. Il lance un niveau en appuyant sur R.

Le joueur joue son niveau. Si il appuie sur ECHAP, il retourne dans le menu.

Si il appuie sur ECHAP dans le menu, celà quitte le jeu.

TODO (prof) schema ici

Lister les niveaux

Il nous faut lister les niveaux dans le répertoire où se trouve le programme.

Pour ça, on peut utilier la fonction listdir du module os de Python, qui nous permet de lister toutes les entrées d'un répertoire (c'est à dire les fichiers et les dossiers). Le module os est déjà importé depuis l'étape précédente, on n'a pas besoin de le réimporter.

entrees = os.listdir(os.path.dirname(__file__))

entrees contient une liste de noms de fichiers présents dans le répertoire. Malheureusement, celà inclut le script courant, et les autres fichiers ou répertoires que l'on aurait pu rajouter. On peut utiliser la syntaxe des listes par compréhension pour selectionner uniquement les fichiers qui contiennent ".lasero".

## On crée une liste de tous les éléments f de entrees qui vérifient ".lasero" in f
fichiers_niveaux = [f for f in entrees if ".lasero" in f]

Finalement, retire son extension à chaque nom de fichier.

noms_niveaux = []
for f in fichiers_niveaux:
    noms_niveaux.append(f.replace(".lasero"), "")

En combinant ces opérations, on crée la fonction lister_niveaux.

def lister_niveaux():
    entrees = os.listdir(os.path.dirname(__file__)) 
    fichiers_niveaux = [f for f in entrees if ".lasero" in f]
    noms_niveaux = []
    for f in fichiers_niveaux:
        noms_niveaux.append(f.replace(".lasero", ""))
    return noms_niveaux

Charger les niveaux

On veut maintenant charger la totalité des niveaux en mémoire, et se rappeler de leur noms respectifs.

On commence donc par ajouter un attribut nom dans la classe Niveau.

class Niveau:
    def __init__(self, nom, personnage, decompte_victoire, projectiles):
        self.nom = nom
        self.personnage = personnage
        self.decompte_victoire = decompte_victoire
        self.projectiles = projectiles

On modifie la fonction charger_niveau pour qu'elle renseigne cet attribut et qu'elle prenne directement le nom du niveau en paramètre.

def charger_niveau(nom):

    with open(chemin_vers_niveau(nom), "r") as f:
        lignes = f.readlines() 

    personnage = analyse_personnage(lignes[0])
    decompte_victoire = analyse_decompte_victoire(lignes[1])
    projectiles = []

    for l in lignes[2:]:
        projectiles.append(analyse_projectile(l))

    return Niveau(nom, personnage, decompte_victoire, projectiles)

On l'utilise en combinaison avec lister_niveaux pour écrire la fonction charger_niveaux, qui renvoie la liste de tous les niveaux.

def charger_niveaux():
    noms_niveaux = lister_niveaux()
    niveaux=[]
    for n in noms_niveaux:
        niveaux.append(charger_niveau(n))
    return niveaux

Principe du menu

On choisit d'afficher le menu comme suit :

Le nom du niveau actuellement sélectionnée est en blanc au centre de l'écran.

Les deux niveaux précédents dans la liste, si ils existent, sont affichés centrés mais décalés vers le haut, avec un blanc plus transparent en fonction de la distance.

Les deux niveaux suivants dans la liste, si ils existent, sont affichés centrés mais décalés vers le bas, avec un blanc plus transparent en fonction de la distance.

TODO (prof) visuel ici

Classe menu

Commençont par créer une classe qui contient toutes les informations du menu. On la met dans une section Menu, qui accueillera également les fonctions de menu.

####################
####    MENU    ####
####################

class Menu:
    def __init__(self, niveaux, ecran):
        # Ces 4 booléens sont mis a True 
        # si le joueur a lancé la commande dans la frame
        self.haut = False 
        self.bas = False 
        self.lancer = False 
        self.quitter = False
        self.niveaux = niveaux
        #indice du niveau sélectionné.dans la liste des niveaux
        self.niveau_selectionne = 0 
        self.ecran = ecran

Boucle principale du menu

La boucle principale du menu ressemble beaucoup à la boucle principale du jeu.

def lancer_menu(niveaux, ecran):
    menu = Menu(niveaux, ecran)

    clock = pygame.time.Clock()


    while not menu.quitter:
        clock.tick(FPS)

        traiter_controles_menu(menu)

        selectionner_niveau_menu(menu)

        lancer_niveau_menu(menu)

        afficher_menu(menu)

Il nous reste simplement à implémenter ces fonctions.

Controles

Pour les contrôles du menu, on va utiliser les même controles que le jeu. On ne crée donc pas de nouvelle constantes.

def traiter_controles_menu(menu):
    ## on remet à zéro les contrôles pour ne pas reprendre
    ## l'état de la frame précédente
    menu.haut = False 
    menu.bas = False 
    menu.lancer = False 
    menu.quitter = False

    for e in pygame.event.get():
            if e.type == pygame.KEYDOWN: 
                if e.key == TOUCHE_QUITTER:
                    menu.quitter = True
                if e.key == TOUCHE_RECOMMENCER:
                    menu.lancer = True
                if e.key == TOUCHE_HAUT:
                    menu.haut = True
                if e.key == TOUCHE_BAS:
                    menu.bas = True

Selection du niveau

Pour selectionner le niveau, on va modifier menu.niveau_selectionne en fonction des commandes menu.haut et menu.bas.

def selectionner_niveau_menu(menu):
    if menu.haut:
        menu.niveau_selectionne -= 1 #on remonte dans la liste
    if menu.bas:
        menu.niveau_selectionne += 1 #on descends dans la liste

    #on vérifie qu'on ne sort pas de la liste
    if menu.niveau_selectionne < 0:
        menu.niveau_selectionne = 0
    if menu.niveau_selectionne > len(menu.niveaux) - 1:
        menu.niveau_selectionne = len(menu.niveaux) - 1

Lancement du niveau

Le lancement du jeu est simple. Si la commande menu.lancer est active, on lance la partie avec jouer_niveau, sur le niveau sélectionné. TODO (prof) reprendre ici (ci dessus)

def lancer_niveau_menu(menu):
    if menu.lancer:
        jouer_niveau(menu.niveaux[menu.niveau_selectionne], menu.ecran)

Centrer et décaler une image

Pour afficher le menu, on a besoin de dessiner une image centrée avec un éventuel décalage vers le haut ou le bas.

C'est pas très compliqué : on reprends la fonction de dessin d'une image centrée qu'on avait écrite au moment de l'écran de fin, et on en fait une copie qui permet de rajouter un tel décalage sous forme d'un vecteur. On ajoute simplement ce vecteur à la position centrée de l'image.

TODO (prof) ajouter schema ici

En Python, ça donne :

def rendre_image_centree_decalee(ecran, image, decalage):
    centre_ecran = Vector2(LARGEUR_FENETRE/2, HAUTEUR_FENETRE/2) 
    centre_image = Vector2(image.get_width()/2, image.get_height()/2)
    ecran.blit(image, centre_ecran - centre_image + decalage)

Fonte et texte

On doit encore une fois charger une fonte. On renseigne la taille et sa couleur dans une constante. On construit un texte par niveau dans la classe menu.

TAILLE_MENU = 50
COULEUR_MENU = (255, 255, 255)
class Menu:
    def __init__(self, niveaux, ecran):
        # Ces 4 booléens sont mis a True 
        # si le joueur a lancé la commande dans la frame
        self.haut = False 
        self.bas = False 
        self.lancer = False 
        self.quitter = False
        self.niveaux = niveaux
        #indice du niveau sélectionné.dans la liste des niveaux
        self.niveau_selectionne = 0 
        self.ecran = ecran

        self.textes_niveaux = {}

        fonte = pygame.font.SysFont("Mono", TAILLE_MENU, True, False)
        for n in niveaux:
            self.textes_niveaux[n.nom] = fonte.render(n.nom, True, COULEUR_MENU)            

Affichage

On spécifie l'espacement attendu et le gain de transparence entre deux lignes du menu dans des constantes.

ESPACEMENT_MENU = 120
GAIN_TRANSPARENCE_MENU = 100

La transparence est souvent appelée canal alpha dans les images. Une valeur d'alpha de 255 équivaut à une couleur opaque, alors que une valeur de 0 représente une couleur transparente.

Dans la fonction d'affichage du menu, après avoir effacé l'écran, on trace le texte du niveau sélectionné, et si besoin des niveaux avant et après dans la liste.

TODO (prof) ici besoin de plus d'explications ?

On crée une fonction intermédiaire pour afficher le nom d'un niveau d'index i, avec un décalage et une transparence.

def rendre_nom_niveau(menu, i, decalage, transparence):
    ecran = menu.ecran
    texte = menu.textes_niveaux[menu.niveaux[i].nom]
    texte.set_alpha(transparence) #on décide de la transparence
    rendre_image_centree_decalee(ecran, texte, Vector2(0, decalage))
def afficher_menu(menu):
    ecran = menu.ecran
    ecran.fill(COULEUR_ARRIERE_PLAN)

    n = menu.niveau_selectionne

    rendre_nom_niveau(menu, n, 0, 255)

    # On dessine les niveaux adjacents si besoin,
    # en ajoutant un décalage de position
    if n > 1:
        rendre_nom_niveau(menu, n - 2, -ESPACEMENT_MENU*2, 255 - GAIN_TRANSPARENCE_MENU * 2)

    if n > 0:
        rendre_nom_niveau(menu, n - 1, -ESPACEMENT_MENU, 255 - GAIN_TRANSPARENCE_MENU)

    if n < len(menu.niveaux) - 1:
        rendre_nom_niveau(menu, n + 1, ESPACEMENT_MENU, 255 - GAIN_TRANSPARENCE_MENU)

    if n < len(menu.niveaux) - 2:
        rendre_nom_niveau(menu, n +2, ESPACEMENT_MENU*2, 255 - GAIN_TRANSPARENCE_MENU * 2)

    pygame.display.flip() 

Fonction main

On n'a plus qu'à éditer la fonction main pour qu'elle charge les niveaux et lance le menu.

def main():
    pygame.init()

    fenetre = pygame.display.set_mode([LARGEUR_FENETRE, HAUTEUR_FENETRE])

    niveaux = charger_niveaux()

    lancer_menu(niveaux, fenetre)

    pygame.quit()

Si on lance le jeu, on peut voir que le menu nous propose le niveau "niveau", puisque c'est le seul que l'on a actuellement.

TODO (prof) capture ici

Le code complet de l'étape se trouve dans le dépliant ci-dessous

Code Complet
import pygame
from pygame.math import Vector2
import os

####################
####   CONFIG   ####
####################

## Général 

NIVEAU = "niveau"

FPS = 60

LARGEUR_FENETRE = 1280
HAUTEUR_FENETRE = 720

## Controles

TOUCHE_QUITTER = pygame.K_ESCAPE
TOUCHE_HAUT = pygame.K_UP  
TOUCHE_BAS = pygame.K_DOWN 
TOUCHE_DROITE = pygame.K_RIGHT 
TOUCHE_GAUCHE = pygame.K_LEFT
TOUCHE_RECOMMENCER = pygame.K_r

## Simulation

VITESSE_PERSONNAGE = 300
TAILLE_PERSONNAGE = 10
DECOMPTE_VICTOIRE = 5.0

## Rendu

COULEUR_ARRIERE_PLAN = (0, 0, 0)
COULEUR_PERSONNAGE = (255, 146, 205)
COULEUR_PROJECTILES = (146, 255, 205)
COULEUR_CHRONOMETRE = (150, 150, 170)
TAILLE_CHRONOMETRE = 30
MARGES_CHRONOMETRE = 20
TAILLE_FIN_DE_PARTIE = 150
COULEUR_FIN_DE_PARTIE = (150, 150, 170)

## Menu

TAILLE_MENU = 50
COULEUR_MENU = (255, 255, 255)
ESPACEMENT_MENU = 120
GAIN_TRANSPARENCE_MENU = 100

####################
####  GENERAL   ####
####################

class GameState :

    def __init__(self):
        self.controles = None # on met a None parce que ces attributs seront initialisés plus tard
        self.simulation = None
        self.rendu = None

####################
#### CONTROLEUR ####
####################

class Controles :
    def __init__(self):
        self.quitter = False
        self.direction = Vector2(0,0)
        self.recommencer = False

def initialiser_controles(etat):
    etat.controles = Controles()

def reinitialiser_controles(etat):
    etat.controles = Controles()

def traiter_controles(etat, t):

    for e in pygame.event.get():
            if e.type == pygame.KEYDOWN: 
                if e.key == TOUCHE_QUITTER:
                    etat.controles.quitter = True
                if e.key == TOUCHE_RECOMMENCER:
                    etat.controles.recommencer = True

    direction = Vector2(0, 0)

    clavier = pygame.key.get_pressed()
    if clavier[TOUCHE_HAUT]:
        direction.y += 1.0
    if clavier[TOUCHE_BAS]:
        direction.y -= 1.0
    if clavier[TOUCHE_DROITE]:
        direction.x += 1.0
    if clavier[TOUCHE_GAUCHE]:
        direction.x -= 1.0

    if direction.length() != 0:
        direction.normalize_ip()

    etat.controles.direction = direction

####################
#### SIMULATION ####
####################

class Personnage:

    def __init__(self, position):
        self.position = position

def copier_personnage(personnage):
    return Personnage(personnage.position.copy())

class Projectile: 

    def __init__(self, position, taille, direction, vitesse):
        self.position = position
        self.direction = direction
        self.taille = taille
        if self.direction.length != 0:
            self.direction.normalize_ip()
        self.vitesse = vitesse

def copier_projectile(projectile):
    return Projectile(projectile.position.copy(), projectile.taille, projectile.direction.copy(), projectile.vitesse)

def copier_projectiles(projectiles):
    copie = []
    for p in projectiles:
        copie.append(copier_projectile(p))
    return copie

class Niveau:
    def __init__(self, nom, personnage, decompte_victoire, projectiles):
        self.nom = nom
        self.personnage = personnage
        self.decompte_victoire = decompte_victoire
        self.projectiles = projectiles

class Simulation:

    def __init__(self, niveau):
        self.personnage = copier_personnage(niveau.personnage)
        self.projectiles = copier_projectiles(niveau.projectiles)
        self.collision = False
        self.decompte_victoire = niveau.decompte_victoire
        self.attente = True

def initialiser_simulation(etat, niveau):
    etat.simulation = Simulation(niveau)

def reinitialiser_simulation(etat, niveau):
    initialiser_simulation(etat, niveau)

def intersection(c1, r1, c2, r2):
    return c1.distance_to(c2) <= r1 + r2

def detecter_collisions(etat):
    perso = etat.simulation.personnage
    for p in etat.simulation.projectiles:
        if intersection(perso.position, TAILLE_PERSONNAGE, p.position, p.taille):
            etat.simulation.collision = True

def deplacer_personnage(etat, t):
    direction = etat.controles.direction
    etat.simulation.personnage.position += direction * VITESSE_PERSONNAGE * t

def limiter_deplacement_personnage(etat):
    p = etat.simulation.personnage
    if p.position.x < TAILLE_PERSONNAGE:
        p.position.x = TAILLE_PERSONNAGE
    if p.position.x > LARGEUR_FENETRE - TAILLE_PERSONNAGE:
        p.position.x = LARGEUR_FENETRE - TAILLE_PERSONNAGE
    if p.position.y < TAILLE_PERSONNAGE:
        p.position.y = TAILLE_PERSONNAGE
    if p.position.y > HAUTEUR_FENETRE - TAILLE_PERSONNAGE:
        p.position.y = HAUTEUR_FENETRE - TAILLE_PERSONNAGE

def deplacer_projectiles(etat, t):
    for p in etat.simulation.projectiles:
        p.position += p.direction * p.vitesse * t

def faire_rebondir_projectiles(etat):
    for p in etat.simulation.projectiles:
        if p.position.x < p.taille:
            p.position.x = p.taille
            p.direction.x *= -1
        if p.position.x > LARGEUR_FENETRE - p.taille:
            p.position.x = LARGEUR_FENETRE - p.taille
            p.direction.x *= -1
        if p.position.y < p.taille:
            p.position.y = p.taille
            p.direction.y *= -1
        if p.position.y > HAUTEUR_FENETRE - p.taille:
            p.position.y = HAUTEUR_FENETRE - p.taille
            p.direction.y *= -1

def decompter_victoire(etat, t):
    etat.simulation.decompte_victoire -= t
    if etat.simulation.decompte_victoire < 0:
        etat.simulation.decompte_victoire = 0.0

def verifier_attente(etat):
    if etat.simulation.attente and etat.controles.direction.length() != 0.0:
        etat.simulation.attente = False

def avancer_simulation(etat, t):
    if etat.simulation.attente:
        verifier_attente(etat)
    elif not etat.simulation.collision and etat.simulation.decompte_victoire != 0.0:
        deplacer_personnage(etat, t)
        limiter_deplacement_personnage(etat)
        deplacer_projectiles(etat, t)
        faire_rebondir_projectiles(etat)
        detecter_collisions(etat)
        decompter_victoire(etat, t)

####################
####   RENDU    ####
####################

def images_caracteres(fonte, caracteres, couleur):
    cars = {}
    for c in caracteres: 
        cars[c] = fonte.render(c, True, couleur)
    return cars

class Rendu:
    def __init__(self, ecran):
        self.ecran = ecran

        fonte_chrono = pygame.font.SysFont("Mono", TAILLE_CHRONOMETRE, True, False)
        self.caracteres_chrono = images_caracteres(fonte_chrono, "0123456789.", COULEUR_CHRONOMETRE)

        fonte_fin = pygame.font.SysFont("Mono", TAILLE_FIN_DE_PARTIE, True, False)
        self.texte_victoire = fonte_fin.render("VICTOIRE", True, COULEUR_FIN_DE_PARTIE)
        self.texte_defaite = fonte_fin.render("DEFAITE", True, COULEUR_FIN_DE_PARTIE)

def ref_sve(v):
    return Vector2(v.x, HAUTEUR_FENETRE - v.y)

def initialiser_rendu(etat, ecran):
    etat.rendu = Rendu(ecran)

def reinitialiser_rendu(etat):
    pass

def rendre_personnage(etat):
    ecran = etat.rendu.ecran
    position = etat.simulation.personnage.position
    pygame.draw.circle(ecran, COULEUR_PERSONNAGE, ref_sve(position), TAILLE_PERSONNAGE)

def rendre_projectiles(etat):
    ecran = etat.rendu.ecran
    for p in etat.simulation.projectiles:
        position = p.position
        pygame.draw.circle(ecran, COULEUR_PROJECTILES, ref_sve(position), p.taille)

def rendre_chronometre(etat):
    chrono = f"{etat.simulation.decompte_victoire:06.2f}" #Conversion en chaine
    caracteres = etat.rendu.caracteres_chrono

    x = MARGES_CHRONOMETRE #x de départ (référentiel écran)
    y = MARGES_CHRONOMETRE #y de départ (référentiel écran)

    for c in chrono:
        etat.rendu.ecran.blit(caracteres[c], (x, y))
        #la methode get_width de l'objet image renvoie la largeur de l'image.
        x += caracteres[c].get_width()

def rendre_image_centree(ecran, image):
    centre_ecran = Vector2(LARGEUR_FENETRE/2, HAUTEUR_FENETRE/2) 
    centre_image = Vector2(image.get_width()/2, image.get_height()/2)
    ecran.blit(image, centre_ecran - centre_image)

def rendre_fin_de_partie(etat):
    ecran = etat.rendu.ecran
    if etat.simulation.collision : 
        #si on a perdu
        rendre_image_centree(ecran, etat.rendu.texte_defaite)
    elif etat.simulation.decompte_victoire == 0.0:
        #si la partie est finie et qu'on a pas perdu
        rendre_image_centree(ecran, etat.rendu.texte_victoire)

def afficher_rendu(etat, t):
    ecran = etat.rendu.ecran
    ecran.fill(COULEUR_ARRIERE_PLAN)

    rendre_chronometre(etat)

    rendre_fin_de_partie(etat)

    rendre_projectiles(etat)

    rendre_personnage(etat)

    pygame.display.flip()  


####################
####   NOYAU    ####
####################

def chemin_vers_niveau(niveau):
    repertoire = os.path.dirname(__file__)
    fichier = niveau + ".lasero" #on ajoute l'extension
    return os.path.join(repertoire, fichier)

def analyse_vecteur(chaine):
    sans_parentheses = chaine.replace("(", "").replace(")", "")
    coords = sans_parentheses.split(",")
    x = float(coords[0])
    y = float(coords[1])
    return Vector2(x, y)

def analyse_personnage(chaine):
    return Personnage(analyse_vecteur(chaine))

def analyse_decompte_victoire(chaine):
    return float(chaine)

def analyse_projectile(chaine):
    separee = chaine.split(" ")
    position = analyse_vecteur(separee[0])
    taille = float(separee[1])
    direction = analyse_vecteur(separee[2])
    vitesse = float(separee[3])
    return Projectile(position, taille, direction, vitesse)

def charger_niveau(nom):

    with open(chemin_vers_niveau(nom), "r") as f:
        lignes = f.readlines() 

    personnage = analyse_personnage(lignes[0])
    decompte_victoire = analyse_decompte_victoire(lignes[1])
    projectiles = []

    for l in lignes[2:]:
        projectiles.append(analyse_projectile(l))

    return Niveau(nom, personnage, decompte_victoire, projectiles)

def lister_niveaux():
    entrees = os.listdir(os.path.dirname(__file__)) 
    fichiers_niveaux = [f for f in entrees if ".lasero" in f]
    noms_niveaux = []
    for f in fichiers_niveaux:
        noms_niveaux.append(f.replace(".lasero", ""))
    return noms_niveaux

def charger_niveaux():
    noms_niveaux = lister_niveaux()
    niveaux=[]
    for n in noms_niveaux:
        niveaux.append(charger_niveau(n))
    return niveaux

def jouer_niveau(niveau, ecran):
    etat = GameState()

    initialiser_controles(etat)
    initialiser_simulation(etat, niveau)
    initialiser_rendu(etat, ecran)

    clock = pygame.time.Clock()

    while not etat.controles.quitter:

        if etat.controles.recommencer:
            reinitialiser_controles(etat)
            reinitialiser_simulation(etat, niveau)
            reinitialiser_rendu(etat)

        t = clock.tick(FPS)/1000

        traiter_controles(etat, t)

        avancer_simulation(etat,t)

        afficher_rendu(etat, t)

####################
####    MENU    ####
####################

class Menu:
    def __init__(self, niveaux, ecran):
        # Ces 4 booléens sont mis a True 
        # si le joueur a lancé la commande dans la frame
        self.haut = False 
        self.bas = False 
        self.lancer = False 
        self.quitter = False
        self.niveaux = niveaux
        #indice du niveau sélectionné.dans la liste des niveaux
        self.niveau_selectionne = 0 
        self.ecran = ecran

        self.textes_niveaux = {}

        fonte = pygame.font.SysFont("Mono", TAILLE_MENU, True, False)
        for n in niveaux:
            self.textes_niveaux[n.nom] = fonte.render(n.nom, True, COULEUR_MENU) 

def traiter_controles_menu(menu):
    ## on remet à zéro les contrôles pour ne pas reprendre
    ## l'état de la frame précédente
    menu.haut = False 
    menu.bas = False 
    menu.lancer = False 
    menu.quitter = False

    for e in pygame.event.get():
            if e.type == pygame.KEYDOWN: 
                if e.key == TOUCHE_QUITTER:
                    menu.quitter = True
                if e.key == TOUCHE_RECOMMENCER:
                    menu.lancer = True
                if e.key == TOUCHE_HAUT:
                    menu.haut = True
                if e.key == TOUCHE_BAS:
                    menu.bas = True

def selectionner_niveau_menu(menu):
    if menu.haut:
        menu.niveau_selectionne -= 1 #on remonte dans la liste
    if menu.bas:
        menu.niveau_selectionne += 1 #on descends dans la liste

    #on vérifie qu'on ne sort pas de la liste
    if menu.niveau_selectionne < 0:
        menu.niveau_selectionne = 0
    if menu.niveau_selectionne > len(menu.niveaux) - 1:
        menu.niveau_selectionne = len(menu.niveaux) - 1

def lancer_niveau_menu(menu):
    if menu.lancer:
        jouer_niveau(menu.niveaux[menu.niveau_selectionne], menu.ecran)

def rendre_image_centree_decalee(ecran, image, decalage):
    centre_ecran = Vector2(LARGEUR_FENETRE/2, HAUTEUR_FENETRE/2) 
    centre_image = Vector2(image.get_width()/2, image.get_height()/2)
    ecran.blit(image, centre_ecran - centre_image + decalage)

def rendre_nom_niveau(menu, i, decalage, transparence):
    ecran = menu.ecran
    texte = menu.textes_niveaux[menu.niveaux[i].nom]
    texte.set_alpha(transparence) #on décide de la transparence
    rendre_image_centree_decalee(ecran, texte, Vector2(0, decalage))

def afficher_menu(menu):
    ecran = menu.ecran
    ecran.fill(COULEUR_ARRIERE_PLAN)

    n = menu.niveau_selectionne

    rendre_nom_niveau(menu, n, 0, 255)

    # On dessine les niveaux adjacents si besoin,
    # en ajoutant un décalage de position
    if n > 1:
        rendre_nom_niveau(menu, n - 2, -ESPACEMENT_MENU*2, 255 - GAIN_TRANSPARENCE_MENU * 2)

    if n > 0:
        rendre_nom_niveau(menu, n - 1, -ESPACEMENT_MENU, 255 - GAIN_TRANSPARENCE_MENU)

    if n < len(menu.niveaux) - 1:
        rendre_nom_niveau(menu, n + 1, ESPACEMENT_MENU, 255 - GAIN_TRANSPARENCE_MENU)

    if n < len(menu.niveaux) - 2:
        rendre_nom_niveau(menu, n +2, ESPACEMENT_MENU*2, 255 - GAIN_TRANSPARENCE_MENU * 2)

    pygame.display.flip() 

def lancer_menu(niveaux, ecran):
    menu = Menu(niveaux, ecran)

    clock = pygame.time.Clock()


    while not menu.quitter:
        clock.tick(FPS)

        traiter_controles_menu(menu)

        selectionner_niveau_menu(menu)

        lancer_niveau_menu(menu)

        afficher_menu(menu)

####################
####    MAIN    ####
####################

def main():
    pygame.init()

    fenetre = pygame.display.set_mode([LARGEUR_FENETRE, HAUTEUR_FENETRE])

    niveaux = charger_niveaux()

    lancer_menu(niveaux, fenetre)

    pygame.quit()

if __name__ == "__main__":
    main()

Niveaux supplémentaires

Vous pouvez télécharger une archive Tar contenant plusieurs niveaux en cliquant sur ce lien.

Extrayez-en le contenu à l'aide d'un logiciel comme 7zip, et placez les fichiers qu'elle contient à côté du script du jeu.

En lançant le jeu, vous verrez les différents niveaux dans le menu. Attention, certains sont très difficiles !

menu multi niveaux