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