Moment cinetique (Carnot 2026, Prepa MP2I)

UNIVERS / THÈME CHOISI:

Thème Tower Defense

MODE D'EMPLOI

Ce jeu est un Tower Defense de l'espace. Le but : protéger votre soleil (12 vies) à gauche de l'écran. Les planètes dont vous disposez tirent des lasers. Avant chaque vague, il faut placer les planètes de manière à dévier la trajectoire des extra-terrestres grâce à l'attraction gravitationnelle et leur tirer dessus. Il y a une infinité de vagues qui deviennent de plus en plus difficiles, alors choisissez bien vos positions. Un extra-terrestre est en capacité de toucher le soleil même après être sorti de l'écran (car ils sont constamment attirés par lui). Les planètes peuvent être détruites (grande : 4 vies, moyenne : 3 vies, petite : 2 vies).

Commandes :

Choisissez une position avec les flèches et Entrée pour passer au positionnement de la planète suivante. Lorsque les planètes sont placées, cette même touche permet de lancer la phase d'invasion.

Lore :

Dans un univers en perdition, des aliens sectaires souhaitent accélérer la chute de notre galaxie, d'astre en astre, éteignant les étoiles une par une. Leur dernière cible est notre soleil, et même si les humains ne font pas le poids, il résisteront jusqu'à leur mort.

Code Python
import pyxel, math

HEIGHT, WIDTH = 256, 256

RAYONS = [5, 10, 20]
DISTANCES = [50, 100, 150]
VIES = [2, 3, 4]
SPAWN_DELAI = 20
SPECS_TOUR = [
    {"delai": 40, "degat": 20},
    {"delai": 30, "degat": 30},
    {"delai": 20, "degat": 40}
]
VIE_ENNEMI = 40
COEF_FROTTEMENT = -0.001
DEGATS_VAISSEAU = 1
VIE_SOLEIL = 12

SPRITE_PLANETE = [[((4,68),(11,75)),((20,67),(28,75)),((34,66),(45,77))], [((2,2),(13,13)),((18,2),(29,13)),((34,2),(45,13)),((50,1),(61,14))],

                  [((0,112),(15,127)),((16,112),(31,127)),((32,112),(47,127)),((48,112),(63,127)),((64,112),(79,127))]]

SPRITE_SOLEIL = [((4,146),(15,173)),((20,146),(31,173)),((36,146),(47,173)),((52,146),(63,173))]
SPRITE_VAISSEAU = [(35,179), (10, 10)]

def nombre_ennemis(numero_vague):
    return 10*numero_vague

def milieu():
    return (WIDTH//2, HEIGHT//2)

def soleil_pos():
    return (WIDTH, HEIGHT//2)

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, autre):
        return Point(self.x + autre.x, self.y + autre.y)

    def __sub__(self, autre):
        return Point(self.x - autre.x, self.y - autre.y)

    def to_vect(self):
        return Vect(self.x, self.y)

    @staticmethod
    def distance(p1, p2):
        return pyxel.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2)

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

class Attracteur:
    def __init__(self, o1, o2, r):
        self.objet_attracteur = o2
        self.objet_attire = o1
        self.r = r
        self.update()

    def update(self):
        d = Point.distance(self.objet_attire.point, self.objet_attracteur.point)
        if d != 0:
            self.r = self.objet_attracteur.masse/d**2
            soustraction = (self.objet_attracteur.point - self.objet_attire.point).to_vect()
            norme = soustraction.norme()
            if norme != 0:
                self.vect = Vect.scal_mul(soustraction, self.r/norme)
            else:
                self.vect = Vect.scal_mul(soustraction, 0)

    def draw(self):
        self.vect.draw(self.objet_attire.point)

class Vect:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, autre):
        return Vect(self.x + autre.x, self.y + autre.y)

    def __sub__(self, autre):
        return Vect(self.x - autre.x, self.y - autre.y)

    @staticmethod
    def scal_mul(v, scal: float):
        return Vect(scal * v.x, scal * v.y)

    def norme(self):
        return pyxel.sqrt(self.x ** 2 + self.y ** 2)

    def __repr__(self):
        return f"Vect({self.x}, {self.y})"

    def draw(self, point):
        pyxel.line(point.x, point.y, point.x + self.x, point.y + self.y, 8)

class Tour:
    def __init__(self, rang):
        self.rang = rang
        self.dernier_tir = 0
        self.a_tuer = None

    def set_planete(self, planete):
        self.planete = planete

    def update(self, ennemis):
        if pyxel.frame_count % SPECS_TOUR[self.rang]["delai"] == 0:
            d_min = float("inf")
            a_tuer = None
            for ennemi in ennemis:
                d = Point.distance(ennemi.point, self.planete.point)
                if d < d_min:
                    d_min = d
                    a_tuer = ennemi
            if a_tuer is not None:
                a_tuer.subir_degats(SPECS_TOUR[self.rang]["degat"])
                self.dernier_tir = pyxel.frame_count
                self.a_tuer = a_tuer

    def draw(self):
        if self.a_tuer is not None and pyxel.frame_count - self.dernier_tir <= 8:
                pyxel.line(self.planete.point.x, self.planete.point.y, self.a_tuer.point.x, self.a_tuer.point.y, 8)

class Ennemi:
    def __init__(self, point: Point, v0: Vect):
        self.point = point
        self.v = v0
        self.attracteurs = []
        self.cote = 10
        self.vie = VIE_ENNEMI

    def ajoute_attracteur(self, astre, norme):
        self.attracteurs.append(Attracteur(self, astre, norme))

    def update_attracteur(self):
        for a in self.attracteurs:
            a.update()

    def vecteur_deplacement(self):
        s = Vect(0,0)
        for a in self.attracteurs:
            if not a.objet_attracteur.detruit():
                s += a.vect

        s += Vect(self.v.x * COEF_FROTTEMENT, self.v.y * COEF_FROTTEMENT)
        return s

    def est_detruit(self):
        return self.vie <= 0

    def subir_degats(self, degats):
        self.vie -= degats

    def box(self):
        return (self.point, self.point + Point(self.cote, self.cote))

    def update(self):
        self.update_attracteur()
        a = self.vecteur_deplacement()
        self.v.x += a.x
        self.v.y += a.y

        self.point.x += self.v.x
        self.point.y += self.v.y

    def draw(self):
        w, h = SPRITE_VAISSEAU[1][0], SPRITE_VAISSEAU[1][1]
        pyxel.blt(self.point.x - w//2, self.point.y - h//2, 0,  SPRITE_VAISSEAU[0][0], SPRITE_VAISSEAU[0][1], SPRITE_VAISSEAU[1][0], SPRITE_VAISSEAU[1][1], rotate = pyxel.rndi(0, 45))

class Astre:
    def __init__(self, dist, theta0, rayon, col, masse, vie, numero, dtheta = 0):
        self.dist = dist
        self.theta = theta0
        xm, ym = soleil_pos()
        self.x = self.dist*pyxel.cos(math.degrees(theta0)) + xm
        self.y = self.dist*pyxel.sin(math.degrees(theta0)) + ym
        self.point = Point(self.x, self.y)
        self.rayon = rayon
        self.col = col
        self.dtheta = dtheta
        self.attracteurs = []
        self.masse = masse
        self.tours = []
        self.vie = vie
        self.numero = numero

    def box(self):
        if self.numero > -1:
            coords_sprite = SPRITE_PLANETE[self.numero][VIES[self.numero] - self.vie]
            return Point(self.x, self.y), Point(self.x + coords_sprite[1][0] - coords_sprite[0][0]+ 1, self.y + coords_sprite[1][1] - coords_sprite[0][1] + 1)
        else:
            coords_sprite = SPRITE_SOLEIL[(VIE_SOLEIL - self.vie)//3 if self.vie > 0 else 3]
            return Point(self.x, self.y), Point(self.x + coords_sprite[1][0] - coords_sprite[0][0]+ 1, self.y + coords_sprite[1][1] - coords_sprite[0][1] + 1)

        return (Point(self.x - self.rayon, self.y - self.rayon), Point(self.x + self.rayon, self.y + self.rayon))

    def subir_degats(self, degats):
        self.vie = max(self.vie - degats, 0)

    def detruit(self):
        return self.vie <= 0

    def ajoute_tour(self, tour):
        tour.set_planete(self)
        self.tours.append(tour)

    def ajoute_attracteur(self, astre, norme):
        self.attracteurs.append(Attracteur(self, astre, norme))

    def update_attracteur(self):
        for a in self.attracteurs:
            a.update()

    def update(self, ennemis):
        xm, ym = soleil_pos()
        self.theta += self.dtheta
        self.x = self.dist*pyxel.cos(math.degrees(self.theta)) + xm
        self.y = self.dist*pyxel.sin(math.degrees(self.theta)) + ym
        self.point.x, self.point.y = self.x, self.y
        self.update_attracteur()
        self.update_tours(ennemis)

    def update_tours(self, ennemis):
        if not self.detruit():
            for tour in self.tours:
                tour.update(ennemis)

    def draw(self):
        if self.numero > -1:
            coords_sprite = SPRITE_PLANETE[self.numero][VIES[self.numero] - self.vie]
            u, v = coords_sprite[1][0] - coords_sprite[0][0]+ 1, coords_sprite[1][1] - coords_sprite[0][1] + 1
            pyxel.blt(self.x - u//2, self.y - v//2, 0,  coords_sprite[0][0], coords_sprite[0][1], u, v)
        else:
            coords_sprite = SPRITE_SOLEIL[(VIE_SOLEIL - self.vie)//3 if self.vie > 0 else 3]
            u, v = coords_sprite[1][0] - coords_sprite[0][0]+ 1, coords_sprite[1][1] - coords_sprite[0][1] + 1
            pyxel.blt(self.x-u//2-6, self.y-v//2, 0,  coords_sprite[0][0], coords_sprite[0][1], coords_sprite[1][0] - coords_sprite[0][0]+ 1, coords_sprite[1][1] - coords_sprite[0][1] + 1)

        for tour in self.tours:
            tour.draw()

    def draw_attracteurs(self):
        for v in self.attracteurs:
            v.draw()

def collisions_box(box1, box2):
    p1, p4 = box1
    p2 = Point(p4.x, p1.y)
    p3 = Point(p1.x, p4.y)

    q1, q4 = box2
    q2 = Point(q4.x, q1.y)
    q3 = Point(q1.x, q4.y)

    for q in [q1,q2,q3,q4]:
        if p1.x <= q.x <= p4.x and p1.y <= q.y <= p4.y:
            return True

    return False

def dans_zone(point):
    return 0 <= point.x < WIDTH and 0 <= point.y < HEIGHT

class App:
    def __init__(self):
        self.soleil = Astre(0, 0, 10, 5, 10, VIE_SOLEIL, -1, dtheta=0)
        self.creer_planetes()
        self.numero_phase = 0

        pyxel.init(HEIGHT,WIDTH)
        pyxel.load("theme.pyxres")
        pyxel.playm(0, loop=True)

        self.init_phase()
        pyxel.run(self.update, self.draw)

    def creer_planetes(self):
        self.planetes = [Astre(DISTANCES[i], math.pi, RAYONS[i], 2, 10, VIES[i], i) for i in range(len(DISTANCES))]
        for planete in self.planetes:
            planete.ajoute_attracteur(self.soleil, 20)
            planete.ajoute_tour(Tour(1))

    def init_phase(self):
        self.placement_actuel = 0
        self.phase_placement = True
        self.placement_planetes()
        self.numero_phase += 1
        self.compteur_ennemis = 0
        self.max_ennemis = nombre_ennemis(self.numero_phase)
        self.ennemis = []
        self.soleil.vie = VIE_SOLEIL

    def placement_planetes(self):
        if self.phase_placement:
            planete = self.planetes[self.placement_actuel]
            dr = 2
            dtheta = 0.05
            if pyxel.btn(pyxel.KEY_LEFT):
                planete.dist += dr
            elif pyxel.btn(pyxel.KEY_RIGHT):
                planete.dist -= dr
            if pyxel.btn(pyxel.KEY_UP):
                planete.theta += dtheta
            elif pyxel.btn(pyxel.KEY_DOWN):
                planete.theta -= dtheta
            if pyxel.btnp(pyxel.KEY_RETURN):
                self.placement_actuel += 1

            if self.placement_actuel == len(self.planetes):
                self.phase_placement = False
                for p in self.planetes:
                    p.dtheta = 0


    def creer_ennemi(self):
        ennemi = Ennemi(Point(0, HEIGHT//2), Vect(1, 0))
        ennemi.ajoute_attracteur(self.soleil, 0.02)
        for planete in self.planetes:
            ennemi.ajoute_attracteur(planete, 20)
        self.ennemis.append(ennemi)

    def update(self):
        if self.soleil.detruit():
            return

        self.soleil.update(self.ennemis)
        self.placement_planetes()
        for planete in self.planetes:
            planete.update(self.ennemis)

        if len(self.ennemis) == 0 and self.compteur_ennemis > 0:
            self.init_phase()
            return


        if not self.phase_placement and pyxel.frame_count % SPAWN_DELAI == 0 and self.compteur_ennemis < self.max_ennemis:
            self.creer_ennemi()
            self.compteur_ennemis += 1

        nouveaux_ennemis = []
        for ennemi in self.ennemis:
            a_ajouter = True
            if collisions_box(self.soleil.box(), ennemi.box()):
                a_ajouter = False
                self.soleil.subir_degats(DEGATS_VAISSEAU)

            for planete in self.planetes:
                if not planete.detruit() and collisions_box(planete.box(), ennemi.box()):
                    a_ajouter = False
                    planete.subir_degats(DEGATS_VAISSEAU)

            if ennemi.est_detruit():
                a_ajouter = False

            if a_ajouter:
                ennemi.update()
                nouveaux_ennemis.append(ennemi)
        self.ennemis = nouveaux_ennemis

    def draw(self):
        pyxel.cls(0)
        if self.soleil.detruit():
            pyxel.text(WIDTH//2, HEIGHT//2, "GAME OVER", 7)
            return
        self.soleil.draw()
        for planete in self.planetes:
            planete.draw()
        for ennemi in self.ennemis:
            ennemi.draw()

        if self.phase_placement:
            pyxel.text(0, 0, "Placez les planetes avec les touches Fleche", 7)
        else:
            pyxel.text(0, 0, f"Phase numero {self.numero_phase}", 7)
            pyxel.text(WIDTH-60, 0, f"Ennemi {self.compteur_ennemis}/{self.max_ennemis}", 15)
            pyxel.text(WIDTH-80, 10, f"Vie Soleil : {self.soleil.vie}/{VIE_SOLEIL}", 15)

app = App()