tower_defense_readable.py

Created by fixem

Created on May 14, 2026

24.7 KB

Une version lisible et commentée de mon jeu de Tower defense. Idéal pour comprendre comment le jeu a été codé.

Version optimisé: https://my.numworks.com/python/fixem/tower_defense_optimized

Bon jeu !!


# ============================================================
# TOWER DEFENSE v2.0 — Par FIXEM
# Calculatrice NumWorks — MicroPython
# ============================================================
 
from kandinsky import fill_rect, draw_string, get_pixel, set_pixel
from ion import keydown
from time import monotonic
from math import sqrt
 
# ── Palette de couleurs ──────────────────────────────────────
ro  = (255,   0,   0)   # rouge
ja  = (252, 216,   0)   # jaune (argent)
bc  = (  0, 255, 255)   # cyan
gr  = (125, 125, 125)   # gris (fond)
no  = (  0,   0,   0)   # noir
bl  = (255, 255, 255)   # blanc
be  = (255, 204, 115)   # beige (chemin)
gr1 = ( 75,  75,  75)   # gris foncé (texte sélectionné)
gr2 = (175, 175, 175)   # gris clair (texte normal)
 
# ── Prix de base des tours (indices 0-4) ─────────────────────
BASE_PRICES = [25, 75, 150, 450, 2500]
 
# ── Noms des tours ───────────────────────────────────────────
TOWER_NAMES = ["Soldat", "Sniper", "Lance-flamme", "Minigunner", "Anihilateur"]
 
# ── Statistiques des ennemis : (PV, vitesse, récompense) ─────
# Types : 0=Lent, 1=Boss, 2=Normal, 3=Rapide
ENEMY_STATS = [
    (10, 0.5,  3),   # Lent
    (50, 0.5, 15),   # Boss
    ( 5, 1.0,  1),   # Normal
    ( 3, 2.0,  2),   # Rapide
]
 
# ── Points de virage du chemin : {distance_parcourue: direction} ──
# Directions : 0=gauche, 1=haut, 2=bas, 3=droite
TURNS = {92:2, 172:0, 252:2, 322:3, 462:1, 592:3, 722:2, 832:3}
 
# ── Statistiques des tours par type et niveau ────────────────
# Format par niveau : (coût_upgrade, dégâts, délai_tir, portée)
# "--" signifie niveau max atteint (pas d'upgrade possible)
TOWER_STATS = [
    # Soldat
    [(50,2,25,2),(100,3,25,2),(200,3,17,2.25),(400,3,17,2.5),
     (800,5,17,2.5),(1600,10,15,2.5),(3200,12,12,2.5),("--",15,12,3),("--",)*4],
    # Sniper
    [(150,10,150,3),(300,20,150,3),(600,50,150,4),(1200,100,150,4),
     (2400,100,75,5),(4800,150,75,5),(9600,150,65,5),("--",200,65,5),("--",)*4],
    # Lance-flamme
    [(250,1,5,1.5),(500,2,5,1.5),(1000,2,3,1.5),(2000,3,3,1.5),
     (4000,3,3,1.75),(8000,4,3,2),(16000,5,3,2),("--",5,2,2.25),("--",)*4],
    # Minigunner
    [(900,2,10,1.5),(1800,3,10,1.5),(3600,3,7,2.5),(7200,4,7,2.5),
     (14400,5,7,3),(28800,6,7,3),(47600,6,7,3.5),("--",6,5,3.5),("--",)*4],
    # Anihilateur (dégâts infinis)
    [(5000,10**100,1000,5),(10000,10**100,950,5.1),(20000,10**100,900,5.2),
     (40000,10**100,800,5.5),(80000,10**100,700,5.75),(160000,10**100,600,6),
     (320000,10**100,500,6),("--",10**100,500,10),("--",)*4],
]
 
# ── Segments du chemin : (x, y, largeur, hauteur, couleur) ───
PATH = [
    (  0, 42, 100, 10, be), ( 90, 52,  10, 80, be),
    ( 10,122,  80, 10, be), ( 10,132,  10, 60, be),
    ( 10,192, 140, 10, be), (150, 62,  10,140, be),
    (160, 62, 120, 10, be), (280, 62,  10,120, be),
    (290,172,  30, 10, be),
]
 
# ── État global de la partie ─────────────────────────────────
tower  = []   # liste des objets Tower placés
enemy  = []   # liste des objets Enemy actifs
Tpos   = []   # positions (x,y) des tours (même index que tower[].ID)
Epos   = []   # positions (x,y) des ennemis (même index que enemy[].ID)
wave   = 0    # numéro de vague courante
money  = 50   # argent du joueur
alive  = 0    # nombre d'ennemis encore en vie
lVl    = 0    # niveau de difficulté global (augmente toutes les 20 vagues)
reset  = True # indique qu'on démarre une nouvelle vague (remet les timers à 0)
Perso  = 0    # 1 = vague personnalisée (mode Training)
 
 
# ============================================================
# CLASSES
# ============================================================
 
class Tower:
    """Tour défensive placée sur la carte."""
    __slots__ = ('ID', 'n', 'Lvl', 'Stats', 'T')
 
    def __init__(self, tower_type, lvl=0):
        self.ID    = len(tower)           # index dans la liste globale
        self.n     = tower_type           # type de tour (0-4)
        self.Lvl   = lvl
        self.Stats = TOWER_STATS[tower_type][lvl]
        self.T     = 0                    # compteur de rechargement
 
    def Draw(self):
        """Affiche le carré coloré représentant la tour."""
        colors = [(255,255,0),(255,0,255),(0,255,255),(255,255,255),(0,0,0)]
        x, y = Tpos[self.ID]
        fill_rect(x+2, y+2, 6, 6, colors[self.n])
 
    def Attack(self, Tpos, enemy, Epos, ready, wave):
        """Cherche la cible la plus avancée dans la portée et lui inflige des dégâts."""
        global reset, alive, money
 
        # Réinitialiser le timer au début d'une vague
        if reset:
            reset = False
            self.T = 0
 
        stats = self.Stats
        range_sq = (stats[3] * 10) ** 2   # portée2 pour éviter sqrt
        cx, cy = Tpos[self.ID]
        cx += 5; cy += 5                   # centre de la tour
 
        # Trouver l'ennemi le plus avancé (Tr = distance parcourue) dans la portée
        best_dist = 0
        target = None
        for i in range(len(Epos)):
            if enemy[i].HP > 0 and enemy[i].Tr > best_dist:
                ex, ey = Epos[i]
                dx = cx - ex - 3
                dy = cy - ey - 3
                if dx*dx + dy*dy <= range_sq:
                    best_dist = enemy[i].Tr
                    target = i
 
        if target is not None and self.T >= stats[2]:
            # Tirer : infliger les dégâts et gérer la mort
            self.T = 0
            e = enemy[target]
            e.dam = 5           # déclenche l'animation de dégâts (rouge)
            e.HP -= stats[1]
            if e.HP <= 0:
                ID = e.ID
                money += e.R
                alive -= 1
                Info(ready, wave, money)
                ex, ey = Epos[ID]
                fill_rect(int(ex), int(ey), 6, 6, be)   # effacer le sprite
                del enemy[target]
                del Epos[ID]
                # Décaler les ID des ennemis suivants
                for e2 in enemy:
                    if e2.ID > ID:
                        e2.ID -= 1
        else:
            self.T += 1   # recharger
 
 
class Enemy:
    """Ennemi qui suit le chemin prédéfini."""
    __slots__ = ('dam', 'n', 'ID', 'Lvl', 'HP', 'V', 'R', 'Tr', 'D', 'Ef')
 
    def __init__(self, enemy_type, lvl):
        base = ENEMY_STATS[enemy_type]
        self.n   = enemy_type
        self.ID  = len(enemy)
        self.Lvl = lvl
        self.HP  = int(base[0] * (1 + 2.5 * lvl))   # PV scalés par le niveau
        self.V   = base[1]                            # vitesse
        self.R   = int(base[2] * 2 * (lvl + 1))      # récompense à la mort
        self.Tr  = -10 * (self.ID + 1)               # distance parcourue (négatif = pas encore entré)
        self.D   = 3                                  # direction initiale : droite
        self.dam = 0                                  # compteur d'animation dégâts
        self.Ef  = 0                                  # flag d'effacement du sprite
 
    def Move(self, Epos):
        """Déplace l'ennemi d'un pas et gère les virages."""
        # Couleurs selon le type et l'état (rouge si vient de recevoir des dégâts)
        type_colors = [(0,150,0), (50,50,50), (100,200,100), (0,150,255)]
        color = ro if self.dam > 0 else type_colors[self.n]
        if self.dam > 0:
            self.dam -= 1
 
        # Changer de direction si on atteint un virage
        if self.Tr in TURNS:
            self.D = TURNS[self.Tr]
 
        ex, ey = Epos[self.ID]
        v = self.V
 
        # Effacer ancienne position
        fill_rect(int(ex), int(ey), 6, 6, be)
 
        # Déplacer selon la direction
        if   self.D == 0: ex -= v
        elif self.D == 1: ey -= v
        elif self.D == 2: ey += v
        else:             ex += v
 
        Epos[self.ID] = (ex, ey)
        fill_rect(int(ex), int(ey), 6, 6, color)
        self.Tr += v
 
 
# ============================================================
# FONCTIONS D'AFFICHAGE
# ============================================================
 
def draw_rects(rects):
    """Dessine une liste de rectangles [(x,y,w,h,couleur)]."""
    for r in rects:
        fill_rect(r[0], r[1], r[2], r[3], r[4])
 
def Info(ready, wave, money):
    """Met à jour la barre d'info en haut de l'écran."""
    mo = "" if money > 10**999 else money
    if ready:
        draw_string("Wave:" + str(wave) + "-In progress", 0, 1, no, gr)
    else:
        draw_string("Wave:" + str(wave) + " "*12, 0, 1, no, gr)
    draw_string("Money:" + str(mo), 260 - 10*len(str(mo)), 1, ja, gr)
    fill_rect(0, 21, 320, 1, bl)
 
def init_screen(ready, wave, money):
    """Redessine entièrement la carte (fond + chemin + tours + HUD)."""
    fill_rect(0, 0, 320, 222, gr)
    Info(ready, wave, money)
    draw_rects(PATH)
    for t in tower:
        t.Draw()
 
def cursor(x, y, color):
    """Dessine le curseur 10×10 à la position donnée (bords colorés, intérieur transparent)."""
    bg = get_pixel(x+2, y)   # couleur du fond pour restaurer l'intérieur
    fill_rect(x,   y,   10,  1, color)   # bord haut
    fill_rect(x,   y,    1, 10, color)   # bord gauche
    fill_rect(x+9, y,    1, 10, color)   # bord droit
    fill_rect(x,   y+9, 10,  1, color)   # bord bas
    # Restaurer les coins intérieurs pour l'effet "creux"
    fill_rect(x+2, y,    6,  1, bg)
    fill_rect(x,   y+2,  1,  6, bg)
    fill_rect(x+9, y+2,  1,  6, bg)
    fill_rect(x+2, y+9,  6,  1, bg)
 
def circle(x0, y0, r, color, thickness):
    """Dessine un cercle (utilisé pour afficher la portée d'une tour)."""
    for i in range(thickness):
        ri = r - i
        ri2 = ri * ri
        xd = x0 - int(ri / 1.4142)
        xf = x0 + int(ri / 1.4142) + 1
        for x in range(xd, xf):
            dx = x - x0
            dy = int(sqrt(ri2 - dx*dx))
            # Dessiner les 4 quadrants par symétrie
            x1, y1 = x, y0 + dy
            for _ in range(4):
                set_pixel(x1, y1, color)
                x1, y1 = x0 + y1 - y0, y0 + x0 - x1
 
def Info2(x, y):
    """Affiche les stats détaillées de la tour sous le curseur. Retourne la couleur du curseur."""
    if (x, y) not in Tpos:
        return bc   # pas de tour ici : curseur cyan
    ID = Tpos.index((x, y))
    t = next((t2 for t2 in tower if t2.ID == ID), None)
    if t is None:
        return bc
    st = t.Stats
    d1 = st[1]
    d2 = TOWER_STATS[t.n][t.Lvl+1][1]
    if d1 > 10000:
        d1 = d2 = ""
    draw_string(TOWER_NAMES[t.n] + "/Level: " + str(t.Lvl),          0, 100)
    draw_string("Upgrade: "      + str(st[0]),                        0, 120)
    draw_string("Damage: "       + str(d1) + "-->" + str(d2),         0, 140)
    draw_string("Reload delay: " + str(st[2]) + "-->" + str(TOWER_STATS[t.n][t.Lvl+1][2]), 0, 160)
    draw_string("Range: "        + str(st[3]) + "-->" + str(TOWER_STATS[t.n][t.Lvl+1][3]), 0, 180)
    return ro   # curseur rouge = tour sélectionnée
 
 
# ============================================================
# LOGIQUE DE VAGUE
# ============================================================
 
def Remplir(Wave=[0, 0, 0, 0]):
    """Génère les ennemis de la vague courante et les place hors-écran à gauche."""
    global alive, reset, Perso, Epos, enemy, lVl, wave
    reset = True
 
    if Perso:
        # Mode Training : vague personnalisée, déjà définie dans Wave
        Perso = 0
    else:
        # Mode normal : composition de la vague calculée depuis le numéro de vague
        cycle = wave % 20          # position dans le cycle de 20 vagues
        lVl   = wave // 20         # niveau de difficulté (1 tous les 20 vagues)
        if   cycle == 0: Wave = [0, 0, 2, 0]
        elif cycle == 1: Wave = [0, 0, 0, 2]
        elif cycle == 2: Wave = [2, 0, 0, 0]
        else:
            Wave = [
                cycle//4  + lVl//3,
                cycle//10 + wave//50,
                cycle//2  + lVl//3,
                cycle//2 - 1 + lVl//3,
            ]
 
    # Créer les ennemis en les décalant hors écran selon leur ordre d'entrée
    spawn_offset = 0
    for enemy_type, count in enumerate(Wave):
        for _ in range(count):
            alive        += 1
            spawn_offset += 1
            Epos.append((-10 * spawn_offset, 44))
            enemy.append(Enemy(enemy_type, lVl))
 
 
# ============================================================
# BOUCLE PRINCIPALE DE JEU
# ============================================================
 
def Play(training_mode=0, Wave=[0, 0, 0, 0]):
    """Boucle de jeu principale. training_mode=1 donne de l'argent infini."""
    global wave, Perso, alive, money, Tpos, tower, enemy, Epos
 
    # Initialisation de la partie
    money  = 10**1000 if training_mode else 50
    Perso  = 1 if training_mode else 0
    wave   = 0
    tower  = []; Tpos = []; Epos = []; enemy = []
    PV     = 100    # points de vie du joueur
    ready  = 0      # 0 = en attente de lancer la vague, 1 = vague en cours
    ts     = 1      # durée de la dernière frame (pour calcul FPS)
 
    # Position et état du curseur
    x, y       = 40, 42
    rd         = 0        # compteur pour limiter la vitesse de déplacement du curseur
    can_place  = 0        # 1 si la case courante est libre pour poser une tour
    eff        = 0        # 1 si on affichait le cercle de portée (besoin de redraw)
    efff       = 0        # flag de redraw après déplacement
 
    # Mapping touche → type de tour et coût
    KEY_TO_TYPE  = {42:0, 43:1, 44:2, 36:3, 37:4}
    KEY_TO_PRICE = {42:25, 43:75, 44:125, 36:450, 37:2500}
    DIR_DELTA    = [-10, -10, 10, 10]   # déplacement curseur : gauche, haut, bas, droite
 
    init_screen(ready, wave, money)
    co = get_pixel(x, y)
    cursor(x, y, ro)
    PATH_COLOR = get_pixel(0, 42)   # couleur du chemin (pour tester si placement interdit)
 
    while PV > 0:
        frame_start = monotonic()
 
        # ── Touches d'information (affichage ponctuel) ────────
        if keydown(14):
            while keydown(14):
                draw_string(str(PV) + " PV", 290 - 10*len(str(PV)), 202, bl, gr)
        elif keydown(12):
            fps = int(1 / ts)
            while keydown(12):
                draw_string(str(fps) + " fps", 280 - 10*len(str(fps)), 202, bl, gr)
        elif keydown(13) and (x, y) in Tpos:
            while keydown(13):
                col = Info2(x, y)
            init_screen(ready, wave, money)
            cursor(x, y, col)
 
        # ── Déplacement du curseur (1 mouvement tous les 5 frames) ──
        rd += 1
        if rd == 5:
            rd = 0
            for k in (0, 1, 2, 3, 52):
                if keydown(k):
                    # Lancer la vague
                    if k == 52 and not ready:
                        ready = 1
                        Info(ready, wave, money)
                        Remplir(Wave) if Perso else Remplir()
 
                    # Déplacer le curseur et le faire reboucler sur les bords
                    cursor(x, y, co)
                    if   k in (0, 3): x += DIR_DELTA[k]
                    elif k in (1, 2): y += DIR_DELTA[k]
                    x = 0   if x > 300 else (310 if x < 0 else x)
                    y = 22  if y > 202 else (212 if y < 22 else y)
 
                    co = get_pixel(x, y)
                    occupied = co == PATH_COLOR or len(tower) >= 30 or (x, y) in Tpos
                    can_place = 0 if occupied else 1
                    cursor(x, y, bc if can_place else ro)
                    efff = 1
                    while keydown(k): pass
 
            # ── Interaction avec une tour existante ───────────
            if (x, y) in Tpos:
                ID = Tpos.index((x, y))
                to = next((t for t in tower if t.ID == ID), None)
                if to:
                    eff = 1
                    # Afficher le cercle de portée
                    circle(x+5, y+5, to.Stats[3]*10, ro, 1)
                    # Améliorer la tour
                    if keydown(4) and to.Lvl < 7:
                        if money >= to.Stats[0]:
                            to.Lvl += 1
                            money  -= to.Stats[0]
                            to.Stats = TOWER_STATS[to.n][to.Lvl]
                            Info(ready, wave, money)
                        while keydown(4): pass
                    # Vendre la tour
                    elif keydown(17):
                        a = to.ID
                        money += int(3 * ((to.Lvl + 1) * (to.n + 1)) ** 2.5)
                        tower.remove(to)
                        Tpos.remove((x, y))
                        for t in tower:
                            if t.ID > a: t.ID -= 1
                        fill_rect(x, y, 10, 10, gr)
            elif eff:
                # On vient de quitter une tour : redessiner
                init_screen(ready, wave, money)
                eff = 0
                cursor(x, y, bc if can_place else ro)
            if efff and eff:
                init_screen(ready, wave, money)
                efff = 0
                cursor(x, y, ro)
 
        # ── Pause ─────────────────────────────────────────────
        if keydown(48):
            input()
            init_screen(ready, wave, money)
 
        # ── Placement d'une nouvelle tour ─────────────────────
        if can_place:
            for k in (42, 43, 44, 36, 37):
                if keydown(k):
                    if money >= KEY_TO_PRICE[k]:
                        tower.append(Tower(KEY_TO_TYPE[k]))
                        Tpos.append((x, y))
                        money -= KEY_TO_PRICE[k]
                        for t in tower: t.Draw()
                    else:
                        draw_string("Money:" + str(money), 260 - 10*len(str(money)), 1, ro, gr)
                    occupied = co == PATH_COLOR or len(tower) >= 30 or (x, y) in Tpos
                    can_place = 0 if occupied else 1
                    cursor(x, y, bc if can_place else ro)
 
        # ── Mise à jour des tours et des ennemis ──────────────
        for t in tower:
            t.Attack(Tpos, enemy, Epos, ready, wave)
 
        for e in enemy:
            e.Move(Epos)
            ex = Epos[e.ID][0]
            if ex >= 322:
                # L'ennemi a atteint la fin : retirer des PV
                PV    -= e.HP
                e.HP   = 0
                alive -= 1
            if e.Ef:
                fill_rect(int(ex), int(Epos[e.ID][1]), 6, 6, be)
                e.Ef = 0
 
        # ── Fin de vague ──────────────────────────────────────
        if alive == 0 and ready:
            if training_mode:
                # En mode Training : attendre une touche puis quitter
                tower.clear(); Tpos.clear(); Epos.clear(); enemy.clear()
                while not keydown(52): pass
                break
            else:
                # Passer à la vague suivante
                wave  += 1
                money += 5 * wave
                ready  = 0
                Epos.clear(); enemy.clear()
                Info(ready, wave, money)
 
        ts = monotonic() - frame_start
 
 
# ============================================================
# MENUS ET ÉCRANS SECONDAIRES
# ============================================================
 
def Help():
    """Affiche l'aide en plusieurs pages, animée."""
    pages = [
        "Tower Defense v2.0\nPar FIXEM",
        "[shift] -> Voir les FPS\n[alpha] -> Stats d'une tour\n[cut]   -> Voir les PV\n[OK]    -> Ameliorer une tour\n[del]   -> Vendre une tour\nFleches -> Deplacer le curseur\n[EXE]   -> Lancer une vague\n1-5     -> Placer des tours",
        "1 -> Soldat       (25)\n2 -> Sniper       (75)\n3 -> Lance-Flamme (125)\n4 -> Minigunner   (450)\n5 -> Anihilateur  (2500)",
    ]
    tp = 1
    col = [0, 255, 255]
    for page in pages:
        fill_rect(0, 0, 320, 222, gr)
        while not keydown(4):
            # Animation de la couleur du texte "continuer"
            if col[2] > 150 and tp == 1: col[2] -= 1
            else:
                col[2] += 1
                tp = 0 if col[2] < 255 else 1
            draw_string(page, 2, 60, bl, gr)
            draw_string("[OK] pour continuer", 65, 202, tuple(col), gr)
        while keydown(4): pass
    fill_rect(0, 0, 320, 222, gr)
 
def Choose():
    """Écran de configuration de vague personnalisée (mode Training).
    Retourne Wave = [nb_lents, nb_boss, nb_normaux, nb_rapides]."""
    global lVl, Perso
    Wave = [0, 0, 0, 0]
    Perso = 1
    pos = 0   # ligne sélectionnée
    labels = ["Lent:   ", "Boss:   ", "Normal: ", "Rapide: ", "Niveau: "]
 
    while not keydown(4):
        # Afficher les valeurs courantes
        for i in range(5):
            draw_string(labels[i], 25, 2 + 30*i, gr1, gr)
            val = str(Wave[i]) if i < 4 else str(lVl)
            draw_string(val, 25 + 10*len(labels[i]), 2 + 30*i, gr1, gr)
 
        # Navigation verticale
        if keydown(1) and pos > 0:
            draw_string("  ", 2, 2 + 30*pos, gr1, gr)
            pos -= 1
            while keydown(1): pass
        elif keydown(2) and pos < 3:
            draw_string("  ", 2, 2 + 30*pos, gr1, gr)
            pos += 1
            while keydown(2): pass
 
        # Modifier la valeur sélectionnée
        if keydown(0) and Wave[pos] > 0:
            Wave[pos] -= 1
            while keydown(0): pass
        elif keydown(3) and sum(Wave) < 40:
            Wave[pos] += 1
            while keydown(3): pass
 
        # Modifier le niveau de difficulté
        if keydown(46) and lVl > 0:
            lVl -= 1
            while keydown(46): pass
        elif keydown(45):
            lVl += 1
            while keydown(45): pass
 
        # Réinitialiser
        if keydown(52):
            Wave = [0, 0, 0, 0]
            lVl = 0
 
        draw_string("->", 2, 2 + 30*pos, gr1, gr)
 
    return Wave
 
def Train():
    """Lance une partie en mode Training avec une vague personnalisée."""
    Wave = Choose()
    Play(training_mode=1, Wave=Wave)
 
def Inventory():
    """Écran de consultation des statistiques de chaque type de tour."""
    colors = [(255,255,0),(255,0,255),(0,255,255),(255,255,255),(0,0,0)]
    fill_rect(0, 0, 320, 222, gr)
    pos = 0
    # Créer des tours temporaires juste pour naviguer dans les stats
    preview = [Tower.__new__(Tower) for _ in range(5)]
    for i, t in enumerate(preview):
        t.n = i; t.Lvl = 0; t.ID = i
        t.Stats = TOWER_STATS[i][0]; t.T = 0
 
    while True:
        t = preview[pos]
        t.Stats = TOWER_STATS[t.n][t.Lvl]
        st = t.Stats
        dmg = "" if st[1] > 100000 else st[1]
 
        draw_string(TOWER_NAMES[pos],                              2,   2, gr1, gr)
        draw_string("Niveau: " + str(t.Lvl) + " "*9,             12 + 10*len(TOWER_NAMES[pos]), 2, gr2, gr)
        draw_string("Prix upgrade: " + str(st[0])  + " "*6,       2,  42, gr2, gr)
        draw_string("Prix de base: " + str(BASE_PRICES[pos])+" "*6, 2, 62, gr2, gr)
        draw_string("Attaque: "      + str(dmg)    + " "*6,        2,  82, gr2, gr)
        draw_string("Delai: "        + str(st[2])  + " "*6,        2, 102, gr2, gr)
        draw_string("Portee: "       + str(st[3])  + " "*6,        2, 122, gr2, gr)
        draw_string("Symbole:",                                    2, 152, gr2, gr)
        fill_rect(92, 147, 30, 30, colors[pos])
 
        # Navigation entre les tours
        if keydown(0):
            pos = (pos - 1) % 5
            while keydown(0): pass
            fill_rect(0, 0, 320, 222, gr)
        elif keydown(3):
            pos = (pos + 1) % 5
            while keydown(3): pass
            fill_rect(0, 0, 320, 222, gr)
 
        # Changer le niveau affiché
        if keydown(1) and t.Lvl < 7:
            t.Lvl += 1
            while keydown(1): pass
        elif keydown(2) and t.Lvl > 0:
            t.Lvl -= 1
            while keydown(2): pass
 
        if keydown(4): break
 
def Menu():
    """Menu principal avec sélection par flèches."""
    global Perso, Wave
    actions = [Play, Train, Inventory, Help]
    labels  = ["Play", "Training", "Towers", "Help"]
    positions = [(35,22), (15,57), (25,77), (35,97)]
    pos = 0
 
    def draw_menu():
        fill_rect(0, 0, 320, 222, gr)
        fill_rect(5,  7, 100, 120, gr1)
        fill_rect(10, 12, 90, 110, gr)
        draw_string("TOWER DEFENSE", 188,  2, gr2, gr)
        draw_string("V2.0",          278, 22, gr2, gr)
        draw_string("Par FIXEM",       2,200, gr2, gr)
 
    draw_menu()
    while True:
        if keydown(1):
            pos = (pos - 1) % len(labels)
            while keydown(1): pass
        elif keydown(2):
            pos = (pos + 1) % len(labels)
            while keydown(2): pass
 
        for i, (lbl, xy) in enumerate(zip(labels, positions)):
            draw_string(lbl, xy[0], xy[1], gr1 if pos == i else gr2, gr)
 
        if keydown(4):
            while keydown(4): pass
            try:
                actions[pos]()
            except KeyboardInterrupt:
                from time import sleep
                sleep(0.4)
                tower.clear(); Tpos.clear(); Epos.clear(); enemy.clear()
                Perso = 0; Wave = [0, 0, 0, 0]
            draw_menu()
 
 
# ── Point d'entrée ───────────────────────────────────────────
try:
    Menu()
except KeyboardInterrupt:
    pass

During your visit to our site, NumWorks needs to install "cookies" or use other technologies to collect data about you in order to:

With the exception of Cookies essential to the operation of the site, NumWorks leaves you the choice: you can accept Cookies for audience measurement by clicking on the "Accept and continue" button, or refuse these Cookies by clicking on the "Continue without accepting" button or by continuing your browsing. You can update your choice at any time by clicking on the link "Manage my cookies" at the bottom of the page. For more information, please consult our cookies policy.