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