#!/usr/bin/env python3 import math import random import sys import array import pygame # Inicialización de Pygame y mezclador de audio pygame.init() SAMPLE_RATE = 22050 pygame.mixer.init(frequency=SAMPLE_RATE, size=-16, channels=1, buffer=512) # Configuración de pantalla WIDTH, HEIGHT = 800, 600 FOV = 320 # Distancia focal SCREEN = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("Tank BattleZone") CLOCK = pygame.time.Clock() FPS = 60 # Configuración de cámara y visor CAMERA_HEIGHT = 1.2 HORIZON_Y = int(HEIGHT * 0.58) # Colores Retro Vectoriales COLOR_BG = (10, 15, 10) COLOR_GREEN = (0, 255, 66) COLOR_DARK_GREEN = (0, 80, 20) COLOR_RED = (255, 40, 40) COLOR_BLUE = (0, 150, 255) COLOR_PURPLE = (180, 0, 255) COLOR_YELLOW = (255, 230, 0) COLOR_WHITE = (255, 255, 255) # --- GENERADOR SINTÉTICO DE EFECTOS DE SONIDO --- def create_sound_sample(generator_func, duration): """Crea un objeto pygame.mixer.Sound generando muestras de audio PCM.""" num_samples = int(SAMPLE_RATE * duration) buf = array.array("h") for i in range(num_samples): t = i / SAMPLE_RATE val = generator_func(t, i, num_samples) val = max(-32768, min(32767, int(val))) buf.append(val) return pygame.mixer.Sound(buffer=buf) # 1. Sonido de disparo (Frecuencia descendente) def gen_shoot(t, i, total): freq = 600 * (1.0 - t / 0.15) env = 1.0 - (t / 0.15) return math.sin(2 * math.pi * freq * t) * 20000 * max(0, env) SOUND_SHOOT = create_sound_sample(gen_shoot, 0.15) # 2. Sonido de impacto enemigo (Explosión / Ruido blanco con caída) def gen_explosion(t, i, total): env = (1.0 - t / 0.4) ** 2 noise = random.uniform(-1.0, 1.0) return noise * 28000 * max(0, env) SOUND_EXPLOSION = create_sound_sample(gen_explosion, 0.4) # 3. Sonido de impacto en nuestro tanque (Golpe seco metálico) def gen_hit_player(t, i, total): freq = 90 * math.exp(-t * 15) env = math.exp(-t * 12) noise = random.uniform(-0.3, 0.3) return (math.sin(2 * math.pi * freq * t) + noise) * 30000 * max(0, env) SOUND_HIT_PLAYER = create_sound_sample(gen_hit_player, 0.35) # 4. Sonido de rebote u obstáculo (Clink metálico agudo) def gen_clink(t, i, total): freq = 1800 env = math.exp(-t * 35) return math.sin(2 * math.pi * freq * t) * 18000 * max(0, env) SOUND_CLINK = create_sound_sample(gen_clink, 0.08) # 5. Sonido de recarga de escudo (Tono ascendente armónico) def gen_powerup(t, i, total): freq = 400 + (t / 0.25) * 800 env = math.sin(math.pi * (t / 0.25)) return math.sin(2 * math.pi * freq * t) * 22000 * max(0, env) SOUND_POWERUP = create_sound_sample(gen_powerup, 0.25) # 6. Sonido espaciado para movimiento y giro def gen_tread_click(t, i, total): period = 0.12 # Intervalo de tiempo entre pulsos phase = t % period if phase < 0.015: freq = 120 env = math.sin(math.pi * (phase / 0.015)) noise = random.uniform(-0.3, 0.3) return (math.sin(2 * math.pi * freq * phase) + noise) * 8000 * env return 0 SOUND_MOVEMENT = create_sound_sample(gen_tread_click, 0.24) # Canal de audio para el movimiento/giro CHANNEL_MOVEMENT = pygame.mixer.Channel(0) def project_3d(x, y, z, player_x, player_y, player_angle): """Proyecta un punto 3D del mundo a coordenadas 2D relativas a la visión del jugador.""" dx = x - player_x dy = y - player_y cos_a = math.cos(player_angle) sin_a = math.sin(player_angle) rx = dx * cos_a - dy * sin_a ry = dx * sin_a + dy * cos_a if ry <= 0.2: return None screen_x = int(WIDTH / 2 + (rx * FOV) / ry) screen_y = int(HORIZON_Y - ((z - CAMERA_HEIGHT) * FOV) / ry) return (screen_x, screen_y) def draw_vector_shape(surface, points_3d, color, p_x, p_y, p_angle, closed=True): """Dibuja una figura 3D compuesta por segmentos vectoriales.""" pts_2d = [] for pt in points_3d: p2d = project_3d(pt[0], pt[1], pt[2], p_x, p_y, p_angle) if p2d is None: return pts_2d.append(p2d) if len(pts_2d) > 1: if closed: pygame.draw.polygon(surface, color, pts_2d, 1) else: pygame.draw.lines(surface, color, False, pts_2d, 1) # --- FUNCIONES AUXILIARES PARA DIBUJAR ICONOS 2D/3D EN PANTALLA DE INICIO --- def draw_pyramid_icon(surface, cx, cy): size, height = 18, 22 apex = (cx, cy - height) base = [ (cx - size, cy + 8), (cx + size, cy + 8), (cx + size - 6, cy - 4), (cx - size + 6, cy - 4) ] pygame.draw.polygon(surface, COLOR_DARK_GREEN, base, 1) for b in base: pygame.draw.line(surface, COLOR_GREEN, b, apex, 1) def draw_tank_icon(surface, cx, cy, color): # Oruga izquierda / derecha pygame.draw.rect(surface, color, (cx - 16, cy - 10, 6, 20), 1) pygame.draw.rect(surface, color, (cx + 10, cy - 10, 6, 20), 1) # Cuerpo central pygame.draw.rect(surface, color, (cx - 10, cy - 8, 20, 16), 1) # Torreta pygame.draw.rect(surface, color, (cx - 6, cy - 5, 12, 10), 1) # Cañón pygame.draw.line(surface, color, (cx, cy - 5), (cx, cy - 18), 2) def draw_diamond_icon(surface, cx, cy): size = 12 pts = [(cx, cy - size), (cx + size, cy), (cx, cy + size), (cx - size, cy)] pygame.draw.polygon(surface, COLOR_YELLOW, pts, 1) pygame.draw.line(surface, COLOR_YELLOW, (cx, cy - size), (cx, cy + size), 1) pygame.draw.line(surface, COLOR_YELLOW, (cx - size, cy), (cx + size, cy), 1) def draw_ufo_icon(surface, cx, cy): # Anillo exterior pygame.draw.ellipse(surface, COLOR_YELLOW, (cx - 18, cy - 6, 36, 12), 1) # Cúpula superior pygame.draw.arc(surface, COLOR_YELLOW, (cx - 10, cy - 12, 20, 12), 0, math.pi, 1) # Cúpula inferior pygame.draw.arc(surface, COLOR_YELLOW, (cx - 8, cy - 2, 16, 10), math.pi, 2 * math.pi, 1) class Pyramid: def __init__(self, x, y, size=3.0, height=4.0): self.x = x self.y = y self.size = size self.height = height self.radius = size / 1.8 def draw(self, surface, p_x, p_y, p_angle): s = self.size / 2 h = self.height apex = (self.x, self.y, h) base = [ (self.x - s, self.y - s, 0), (self.x + s, self.y - s, 0), (self.x + s, self.y + s, 0), (self.x - s, self.y + s, 0), ] draw_vector_shape(surface, base, COLOR_DARK_GREEN, p_x, p_y, p_angle, True) for b in base: draw_vector_shape( surface, [b, apex], COLOR_GREEN, p_x, p_y, p_angle, False ) class ShieldPowerup: def __init__(self, x, y): self.x = x self.y = y self.z = 1.0 self.radius = 1.2 self.alive = True self.rot = 0.0 def update(self): self.rot += 0.05 def draw(self, surface, p_x, p_y, p_angle): # Diamante flotante amarillo giratorio s = 0.8 h = 0.8 cos_r = math.cos(self.rot) sin_r = math.sin(self.rot) local_pts = [ (-s, 0, 0), (0, -s, 0), (s, 0, 0), (0, s, 0) ] base_3d = [] for lx, ly, lz in local_pts: rx = lx * cos_r - ly * sin_r ry = lx * sin_r + ly * cos_r base_3d.append((self.x + rx, self.y + ry, self.z)) top = (self.x, self.y, self.z + h) bottom = (self.x, self.y, self.z - h) draw_vector_shape(surface, base_3d, COLOR_YELLOW, p_x, p_y, p_angle, True) for pt in base_3d: draw_vector_shape(surface, [pt, top], COLOR_YELLOW, p_x, p_y, p_angle, False) draw_vector_shape(surface, [pt, bottom], COLOR_YELLOW, p_x, p_y, p_angle, False) class UFO: def __init__(self, player_x, player_y, player_angle): self.alive = True self.radius = 2.0 self.score_value = 3000 # Se engendra a una distancia fija al frente y se desplaza perpendicularmente distance = 35.0 direction = random.choice([-1, 1]) # -1: de derecha a izquierda, 1: de izquierda a derecha # Posición central frente a la mirada del jugador center_x = player_x + math.sin(player_angle) * distance center_y = player_y + math.cos(player_angle) * distance # Perpendicular a la dirección de la cámara perp_angle = player_angle + math.pi / 2 offset = 30.0 * direction self.x = center_x - math.sin(perp_angle) * offset self.y = center_y - math.cos(perp_angle) * offset self.z = 2.5 speed = 0.12 self.vx = math.sin(perp_angle) * offset / abs(offset) * speed self.vy = math.cos(perp_angle) * offset / abs(offset) * speed # Contador de vida útil para desaparecer tras cruzar la pantalla self.lifetime = 500 def update(self): self.x += self.vx self.y += self.vy self.lifetime -= 1 if self.lifetime <= 0: self.alive = False def draw(self, surface, p_x, p_y, p_angle): # Dibuja la silueta de Platillo Volador / OVNI vectorial retro r = 1.8 h = 0.6 # Anillo exterior ring_local = [ (r, 0, 0), (r * 0.7, r * 0.7, 0), (0, r, 0), (-r * 0.7, r * 0.7, 0), (-r, 0, 0), (-r * 0.7, -r * 0.7, 0), (0, -r, 0), (r * 0.7, -r * 0.7, 0) ] ring_3d = [(self.x + lx, self.y + ly, self.z) for lx, ly, lz in ring_local] # Cúpula superior e inferior top = (self.x, self.y, self.z + h) bottom = (self.x, self.y, self.z - h) draw_vector_shape(surface, ring_3d, COLOR_YELLOW, p_x, p_y, p_angle, True) for pt in ring_3d[::2]: draw_vector_shape(surface, [pt, top], COLOR_YELLOW, p_x, p_y, p_angle, False) draw_vector_shape(surface, [pt, bottom], COLOR_YELLOW, p_x, p_y, p_angle, False) class EnemyTank: def __init__(self, x, y, enemy_type="RED"): self.x = x self.y = y self.enemy_type = enemy_type # Atributos según el tipo de tanque if enemy_type == "RED": self.hp = 1 self.color = COLOR_RED self.score_value = 1000 elif enemy_type == "BLUE": self.hp = 2 self.color = COLOR_BLUE self.score_value = 2500 elif enemy_type == "PURPLE": self.hp = 3 self.color = COLOR_PURPLE self.score_value = 5000 self.angle = random.uniform(0, math.pi * 2) self.turret_angle = self.angle self.speed = 0.03 self.shoot_cooldown = random.randint(120, 240) self.alive = True self.size = 2.5 self.radius = 1.5 def update(self, player_x, player_y, bullets_list, obstacles, enemies, player_radius): dx = player_x - self.x dy = player_y - self.y target_angle = math.atan2(dx, dy) diff_turret = target_angle - self.turret_angle diff_turret = (diff_turret + math.pi) % (2 * math.pi) - math.pi self.turret_angle += max(-0.03, min(0.03, diff_turret)) diff_body = target_angle - self.angle diff_body = (diff_body + math.pi) % (2 * math.pi) - math.pi self.angle += max(-0.015, min(0.015, diff_body)) dist = math.hypot(dx, dy) if dist > 6.0: next_x = self.x + math.sin(self.angle) * self.speed next_y = self.y + math.cos(self.angle) * self.speed # Comprobar colisiones con obstáculos can_move = True for obstacle in obstacles: min_dist = obstacle.radius + self.radius if math.hypot(next_x - obstacle.x, next_y - obstacle.y) < min_dist: can_move = False break # Comprobar colisiones con otros tanques enemigos if can_move: for other in enemies: if other is not self and other.alive: min_dist = other.radius + self.radius if math.hypot(next_x - other.x, next_y - other.y) < min_dist: can_move = False break # Comprobar colisión con el tanque del jugador if can_move: min_dist = player_radius + self.radius if math.hypot(next_x - player_x, next_y - player_y) < min_dist: can_move = False if can_move: self.x = next_x self.y = next_y if self.shoot_cooldown > 0: self.shoot_cooldown -= 1 else: if dist < 40.0: bullets_list.append( Bullet(self.x, self.y, self.turret_angle, is_player=False) ) SOUND_SHOOT.play() self.shoot_cooldown = random.randint(180, 300) def draw(self, surface, p_x, p_y, p_angle): cos_b = math.cos(self.angle) sin_b = math.sin(self.angle) def transform_body(px, py, pz): rx = px * cos_b - py * sin_b ry = px * sin_b + py * cos_b return (self.x + rx, self.y + ry, pz) cos_t = math.cos(self.turret_angle) sin_t = math.sin(self.turret_angle) def transform_turret(px, py, pz): rx = px * cos_t - py * sin_t ry = px * sin_t + py * cos_t return (self.x + rx, self.y + ry, pz) body_local = [ (-1.2, -1.5, 0), (1.2, -1.5, 0), (1.2, 1.5, 0), (-1.2, 1.5, 0), (-1.0, -1.2, 0.8), (1.0, -1.2, 0.8), (1.0, 1.2, 0.8), (-1.0, 1.2, 0.8) ] body_3d = [transform_body(*pt) for pt in body_local] turret_local = [ (-0.6, -0.6, 0.8), (0.6, -0.6, 0.8), (0.6, 0.6, 0.8), (-0.6, 0.6, 0.8), (-0.4, -0.4, 1.4), (0.4, -0.4, 1.4), (0.4, 0.4, 1.4), (-0.4, 0.4, 1.4) ] turret_3d = [transform_turret(*pt) for pt in turret_local] cannon_3d = [transform_turret(0, 0.4, 1.1), transform_turret(0, 2.6, 1.1)] draw_vector_shape(surface, body_3d[:4], self.color, p_x, p_y, p_angle) draw_vector_shape(surface, body_3d[4:], self.color, p_x, p_y, p_angle) for i in range(4): draw_vector_shape( surface, [body_3d[i], body_3d[i + 4]], self.color, p_x, p_y, p_angle, False ) draw_vector_shape(surface, turret_3d[4:], self.color, p_x, p_y, p_angle) for i in range(4): draw_vector_shape( surface, [turret_3d[i], turret_3d[i + 4]], self.color, p_x, p_y, p_angle, False ) draw_vector_shape(surface, cannon_3d, self.color, p_x, p_y, p_angle, False) class Bullet: def __init__(self, x, y, angle, is_player=True): self.x = x self.y = y self.z = 0.8 self.angle = angle self.speed = 1.2 self.max_distance = 80.0 self.traveled = 0.0 self.is_player = is_player self.alive = True def update(self): self.x += math.sin(self.angle) * self.speed self.y += math.cos(self.angle) * self.speed self.traveled += self.speed if self.traveled >= self.max_distance: self.alive = False def draw(self, surface, p_x, p_y, p_angle): length = 1.8 front_x = self.x + math.sin(self.angle) * length front_y = self.y + math.cos(self.angle) * length p_back = project_3d(self.x, self.y, self.z, p_x, p_y, p_angle) p_front = project_3d(front_x, front_y, self.z, p_x, p_y, p_angle) color = COLOR_WHITE if self.is_player else COLOR_RED if p_back and p_front: pygame.draw.line(surface, color, p_back, p_front, 2) class Game: def __init__(self): self.player_x = 0.0 self.player_y = 0.0 self.player_angle = 0.0 self.player_radius = 1.5 self.move_speed = 0.12 self.rot_speed = 0.03 self.shoot_cooldown = 0 self.flash_timer = 0 self.shield = 10 # Escudo / Vidas del jugador self.score = 0 self.kills = 0 # Contador de tanques destruidos self.initial_enemies = 2 self.spawn_delay = 0 # Temporizador para controlar la velocidad de reaparición self.ufo = None # Instancia del OVNI / Platillo volante self.ufo_spawn_timer = random.randint(600, 1200) # Cada 10-20 segundos self.bullets = [] self.powerups = [] self.enemies = [] # Obstáculos self.obstacles = [ Pyramid(-15, 15), Pyramid(20, 10), Pyramid(-10, 40), Pyramid(15, 35), Pyramid(0, 50), ] self.font = pygame.font.SysFont("monospace", 15, bold=True) self.font_title = pygame.font.SysFont("monospace", 28, bold=True) self.game_over = False self.in_start_screen = True # Estado para la pantalla de inicio # Generar enemigos iniciales for _ in range(self.initial_enemies): self.spawn_enemy() def draw_start_screen(self): """Muestra las instrucciones del juego con iconos vectoriales en la pantalla inicial.""" SCREEN.fill(COLOR_BG) title_surf = self.font_title.render("TANK BATTLEZONE", True, COLOR_GREEN) SCREEN.blit(title_surf, (WIDTH // 2 - title_surf.get_width() // 2, 25)) y_offset = 70 lines_ctrl = [ ("CONTROLES:", COLOR_YELLOW), (" [ARRIBA/W][ABAJO/S] : Avanzar / Retroceder", COLOR_WHITE), (" [IZQ/A][DER/D] : Girar tanque", COLOR_WHITE), (" [ESPACIO] : Disparar", COLOR_WHITE), ] for text, color in lines_ctrl: line_surf = self.font.render(text, True, color) SCREEN.blit(line_surf, (40, y_offset)) y_offset += 20 y_offset += 10 hdr_surf = self.font.render("ELEMENTOS DEL JUEGO:", True, COLOR_YELLOW) SCREEN.blit(hdr_surf, (40, y_offset)) y_offset += 28 # Lista de items con sus respectivos dibujadores de iconos items = [ (draw_pyramid_icon, "PIRAMIDE : Obstaculo infranqueable", COLOR_DARK_GREEN), (lambda s, cx, cy: draw_tank_icon(s, cx, cy, COLOR_RED), "TANQUE ROJO : 1 impacto (1000 Pts)", COLOR_RED), (lambda s, cx, cy: draw_tank_icon(s, cx, cy, COLOR_BLUE), "TANQUE AZUL : 2 impactos (2500 Pts)", COLOR_BLUE), (lambda s, cx, cy: draw_tank_icon(s, cx, cy, COLOR_PURPLE), "TANQUE PURPURA : 3 impactos (5000 Pts) - Suelta escudo", COLOR_PURPLE), (draw_diamond_icon, "DIAMANTE AMARILLO : Escudo (+1 SHIELD / 500 Pts)", COLOR_YELLOW), (draw_ufo_icon, "OVNI AMARILLO : Cruzador - ¡Rellena ESCUDO MAX! (3000 Pts)", COLOR_YELLOW), ] icon_x = 60 text_x = 95 for draw_fn, text, color in items: draw_fn(SCREEN, icon_x, y_offset + 10) line_surf = self.font.render(text, True, color) SCREEN.blit(line_surf, (text_x, y_offset)) y_offset += 38 y_offset += 15 start_surf = self.font.render("PRESIONA INTRO PARA COMENZAR", True, COLOR_GREEN) SCREEN.blit(start_surf, (WIDTH // 2 - start_surf.get_width() // 2, y_offset)) pygame.display.flip() def get_next_enemy_type(self): """Calcula el tipo de enemigo a generar según la progresión de bajas.""" if self.kills >= 25: return "PURPLE" prob_purple = min(0.8, self.kills / 25.0) prob_blue = min(0.6, self.kills / 15.0) r = random.random() if r < prob_purple: return "PURPLE" elif r < prob_purple + prob_blue: return "BLUE" else: return "RED" def spawn_enemy(self): """Genera un nuevo enemigo a una distancia razonable del jugador sin solaparse con obstáculos.""" e_type = self.get_next_enemy_type() for _ in range(50): spawn_angle = random.uniform(0, math.pi * 2) spawn_dist = random.uniform(25, 45) new_x = self.player_x + math.sin(spawn_angle) * spawn_dist new_y = self.player_y + math.cos(spawn_angle) * spawn_dist # Verificar que no aparezca dentro de un obstáculo valid = True for obstacle in self.obstacles: if math.hypot(new_x - obstacle.x, new_y - obstacle.y) < obstacle.radius + 2.0: valid = False break if valid: self.enemies.append(EnemyTank(new_x, new_y, enemy_type=e_type)) break def shoot(self): if self.shoot_cooldown <= 0 and not self.game_over and not self.in_start_screen: start_x = self.player_x + math.sin(self.player_angle) * 1.0 start_y = self.player_y + math.cos(self.player_angle) * 1.0 self.bullets.append(Bullet(start_x, start_y, self.player_angle, is_player=True)) self.shoot_cooldown = 15 self.flash_timer = 3 SOUND_SHOOT.play() def draw_mountains(self): mountain_profile = [ 0, 15, 30, 10, 0, 25, 45, 15, 0, 10, 20, 5, 0, 35, 15, 0 ] step = WIDTH / (len(mountain_profile) - 1) angle_offset = (math.degrees(self.player_angle) * 4) % WIDTH pts = [] for i, h in enumerate(mountain_profile): x = (i * step - angle_offset) % WIDTH y = HORIZON_Y - h pts.append((x, y)) sorted_pts = sorted(pts, key=lambda p: p[0]) for i in range(len(sorted_pts) - 1): if abs(sorted_pts[i][0] - sorted_pts[i + 1][0]) < step * 2: pygame.draw.line( SCREEN, COLOR_DARK_GREEN, sorted_pts[i], sorted_pts[i + 1], 1 ) def draw_radar(self): radar_cx, radar_cy, radar_r = WIDTH // 2, 45, 35 pygame.draw.circle(SCREEN, COLOR_BG, (radar_cx, radar_cy), radar_r) pygame.draw.circle(SCREEN, COLOR_GREEN, (radar_cx, radar_cy), radar_r, 1) fov_rad = math.radians(30) pygame.draw.line( SCREEN, COLOR_DARK_GREEN, (radar_cx, radar_cy), ( radar_cx + int(radar_r * math.sin(-fov_rad)), radar_cy - int(radar_r * math.cos(-fov_rad)), ), ) pygame.draw.line( SCREEN, COLOR_DARK_GREEN, (radar_cx, radar_cy), ( radar_cx + int(radar_r * math.sin(fov_rad)), radar_cy - int(radar_r * math.cos(fov_rad)), ), ) for enemy in self.enemies: if not enemy.alive: continue dx = enemy.x - self.player_x dy = enemy.y - self.player_y cos_a = math.cos(self.player_angle) sin_a = math.sin(self.player_angle) rx = dx * cos_a - dy * sin_a ry = dx * sin_a + dy * cos_a dist = math.hypot(rx, ry) if dist < 60.0: blip_x = radar_cx + int((rx / 60.0) * radar_r) blip_y = radar_cy - int((ry / 60.0) * radar_r) if math.hypot(blip_x - radar_cx, blip_y - radar_cy) <= radar_r - 2: pygame.draw.circle(SCREEN, enemy.color, (blip_x, blip_y), 2) # Mostrar OVNI en el radar if self.ufo and self.ufo.alive: dx = self.ufo.x - self.player_x dy = self.ufo.y - self.player_y cos_a = math.cos(self.player_angle) sin_a = math.sin(self.player_angle) rx = dx * cos_a - dy * sin_a ry = dx * sin_a + dy * cos_a dist = math.hypot(rx, ry) if dist < 60.0: blip_x = radar_cx + int((rx / 60.0) * radar_r) blip_y = radar_cy - int((ry / 60.0) * radar_r) if math.hypot(blip_x - radar_cx, blip_y - radar_cy) <= radar_r - 2: pygame.draw.circle(SCREEN, COLOR_YELLOW, (blip_x, blip_y), 3) def draw_hud(self): pygame.draw.line( SCREEN, COLOR_DARK_GREEN, (0, HORIZON_Y), (WIDTH, HORIZON_Y), 1 ) cx, cy = WIDTH // 2, HORIZON_Y if self.flash_timer > 0: pygame.draw.circle(SCREEN, COLOR_WHITE, (cx, cy), 12, 1) self.flash_timer -= 1 pygame.draw.line(SCREEN, COLOR_GREEN, (cx - 15, cy), (cx - 5, cy), 1) pygame.draw.line(SCREEN, COLOR_GREEN, (cx + 5, cy), (cx + 15, cy), 1) pygame.draw.line(SCREEN, COLOR_GREEN, (cx, cy - 15), (cx, cy - 5), 1) pygame.draw.line(SCREEN, COLOR_GREEN, (cx, cy + 5), (cx, cy + 15), 1) score_surf = self.font.render(f"SCORE: {self.score:06d}", True, COLOR_GREEN) kills_surf = self.font.render(f"KILLS: {self.kills:03d}", True, COLOR_GREEN) shield_surf = self.font.render(f"SHIELD: {'I ' * self.shield}", True, COLOR_YELLOW) SCREEN.blit(score_surf, (20, 20)) SCREEN.blit(kills_surf, (20, 40)) SCREEN.blit(shield_surf, (20, 60)) if self.game_over: go_surf = self.font_title.render("GAME OVER", True, COLOR_RED) score_final_surf = self.font.render(f"PUNTUACION FINAL: {self.score:06d}", True, COLOR_WHITE) kills_final_surf = self.font.render(f"TANQUES DESTRUIDOS: {self.kills:03d}", True, COLOR_WHITE) restart_surf = self.font.render("PRESIONA R PARA VOLVER A JUGAR", True, COLOR_YELLOW) SCREEN.blit(go_surf, (WIDTH // 2 - go_surf.get_width() // 2, HEIGHT // 2 - 60)) SCREEN.blit(score_final_surf, (WIDTH // 2 - score_final_surf.get_width() // 2, HEIGHT // 2 - 10)) SCREEN.blit(kills_final_surf, (WIDTH // 2 - kills_final_surf.get_width() // 2, HEIGHT // 2 + 15)) SCREEN.blit(restart_surf, (WIDTH // 2 - restart_surf.get_width() // 2, HEIGHT // 2 + 50)) self.draw_radar() def reset_game(self): self.player_x = 0.0 self.player_y = 0.0 self.player_angle = 0.0 self.shield = 10 self.score = 0 self.kills = 0 self.spawn_delay = 0 self.ufo = None self.ufo_spawn_timer = random.randint(600, 1200) self.bullets.clear() self.powerups.clear() self.enemies.clear() self.game_over = False for _ in range(self.initial_enemies): self.spawn_enemy() def try_move_player(self, dx, dy): """Intenta mover al jugador comprobando colisiones físicas con obstáculos y tanques enemigos.""" next_x = self.player_x + dx next_y = self.player_y + dy # Colisión con obstáculos (Pirámides) for obstacle in self.obstacles: min_dist = obstacle.radius + self.player_radius if math.hypot(next_x - obstacle.x, next_y - obstacle.y) < min_dist: SOUND_CLINK.play() return # Colisión con tanques enemigos for enemy in self.enemies: if enemy.alive: min_dist = enemy.radius + self.player_radius if math.hypot(next_x - enemy.x, next_y - enemy.y) < min_dist: SOUND_CLINK.play() return self.player_x = next_x self.player_y = next_y def run(self): running = True while running: CLOCK.tick(FPS) # Control de la pantalla de inicio if self.in_start_screen: self.draw_start_screen() for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_RETURN: self.in_start_screen = False continue if self.shoot_cooldown > 0: self.shoot_cooldown -= 1 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: self.shoot() elif event.key == pygame.K_r and self.game_over: self.reset_game() keys = pygame.key.get_pressed() # Movimiento y Control if not self.game_over: moving_fwd_back = keys[pygame.K_UP] or keys[pygame.K_w] or keys[pygame.K_DOWN] or keys[pygame.K_s] turning = keys[pygame.K_LEFT] or keys[pygame.K_a] or keys[pygame.K_RIGHT] or keys[pygame.K_d] if keys[pygame.K_UP] or keys[pygame.K_w]: dx = math.sin(self.player_angle) * self.move_speed dy = math.cos(self.player_angle) * self.move_speed self.try_move_player(dx, dy) if keys[pygame.K_DOWN] or keys[pygame.K_s]: dx = -math.sin(self.player_angle) * self.move_speed dy = -math.cos(self.player_angle) * self.move_speed self.try_move_player(dx, dy) if keys[pygame.K_LEFT] or keys[pygame.K_a]: self.player_angle -= self.rot_speed if keys[pygame.K_RIGHT] or keys[pygame.K_d]: self.player_angle += self.rot_speed if moving_fwd_back or turning: if not CHANNEL_MOVEMENT.get_busy(): CHANNEL_MOVEMENT.play(SOUND_MOVEMENT, loops=-1) else: CHANNEL_MOVEMENT.stop() else: CHANNEL_MOVEMENT.stop() # Gestión de reaparición progresiva de enemigos if not self.game_over: max_enemies = min(6, self.initial_enemies + (self.kills // 8)) if len(self.enemies) < max_enemies: if self.spawn_delay <= 0: self.spawn_enemy() self.spawn_delay = 180 else: self.spawn_delay -= 1 # Temporizador de aparición del OVNI if self.ufo is None or not self.ufo.alive: if self.ufo_spawn_timer <= 0: self.ufo = UFO(self.player_x, self.player_y, self.player_angle) self.ufo_spawn_timer = random.randint(900, 1800) # 15-30 segundos para la siguiente aparición else: self.ufo_spawn_timer -= 1 else: self.ufo.update() # Actualización de Powerups for pw in self.powerups[:]: pw.update() if math.hypot(pw.x - self.player_x, pw.y - self.player_y) < pw.radius + 0.5: if self.shield < 10: self.shield = min(10, self.shield + 1) self.score += 500 SOUND_POWERUP.play() self.powerups.remove(pw) # Actualización y Colisiones de Proyectiles for bullet in self.bullets[:]: bullet.update() if not bullet.alive: self.bullets.remove(bullet) continue # Colisión con obstáculos (Pirámides) hit_obstacle = False for obstacle in self.obstacles: if math.hypot(bullet.x - obstacle.x, bullet.y - obstacle.y) < obstacle.radius: bullet.alive = False hit_obstacle = True SOUND_CLINK.play() break if hit_obstacle: continue # Colisión con el OVNI if bullet.is_player and self.ufo and self.ufo.alive: if math.hypot(bullet.x - self.ufo.x, bullet.y - self.ufo.y) < self.ufo.radius: bullet.alive = False self.ufo.alive = False self.score += self.ufo.score_value self.shield = 10 # Recarga el escudo al máximo (10) SOUND_POWERUP.play() SOUND_EXPLOSION.play() continue # Colisión con el jugador if not bullet.is_player and not self.game_over: if math.hypot(bullet.x - self.player_x, bullet.y - self.player_y) < 1.5: bullet.alive = False self.shield -= 1 SOUND_HIT_PLAYER.play() if self.shield <= 0: self.game_over = True continue # Colisión con enemigo if bullet.is_player: for enemy in self.enemies: if ( enemy.alive and math.hypot(bullet.x - enemy.x, bullet.y - enemy.y) < enemy.size ): bullet.alive = False enemy.hp -= 1 SOUND_CLINK.play() if enemy.hp <= 0: enemy.alive = False self.score += enemy.score_value self.kills += 1 SOUND_EXPLOSION.play() if enemy.enemy_type == "PURPLE" and random.random() < 0.5: self.powerups.append(ShieldPowerup(enemy.x, enemy.y)) self.enemies = [e for e in self.enemies if e.alive] self.spawn_delay = 180 break if not self.game_over: for enemy in self.enemies: if enemy.alive: enemy.update( self.player_x, self.player_y, self.bullets, self.obstacles, self.enemies, self.player_radius, ) SCREEN.fill(COLOR_BG) self.draw_mountains() for obstacle in self.obstacles: obstacle.draw(SCREEN, self.player_x, self.player_y, self.player_angle) for pw in self.powerups: pw.draw(SCREEN, self.player_x, self.player_y, self.player_angle) if self.ufo and self.ufo.alive: self.ufo.draw(SCREEN, self.player_x, self.player_y, self.player_angle) for enemy in self.enemies: if enemy.alive: enemy.draw(SCREEN, self.player_x, self.player_y, self.player_angle) for bullet in self.bullets: bullet.draw(SCREEN, self.player_x, self.player_y, self.player_angle) self.draw_hud() pygame.display.flip() pygame.quit() sys.exit() if __name__ == "__main__": Game().run()