import math import os import sys import array import random import pygame # Inicialización de Pygame y el mezclador de audio pygame.init() pygame.mixer.init(frequency=22050, size=-16, channels=1) # Configuración de Pantalla SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 FPS = 60 # Isometría y Cuadrícula TILE_WIDTH = 64 TILE_HEIGHT = 32 GRID_SIZE_X = 8 GRID_SIZE_Y = 120 # Colores del Entorno COLOR_BG = (15, 15, 25) COLOR_ROAD = (50, 50, 65) COLOR_GRID = (30, 30, 45) # Paleta de Colores COLOR_YELLOW = (255, 215, 0) COLOR_WHITE = (240, 240, 240) COLOR_PURPLE = (100, 30, 140) COLOR_RED = (220, 40, 40) COLOR_BROWN = (139, 69, 19) COLOR_ORANGE = (255, 140, 0) COLOR_CYAN = (0, 200, 255) def generate_sound(sound_type): """Genera efectos de sonido retro de forma sintética sin archivos externos.""" sample_rate = 22050 buffer = array.array("h") if sound_type == "shoot": # Disparo: Tono descendente agudo rápido duration = 0.1 total_samples = int(sample_rate * duration) for i in range(total_samples): t = i / total_samples freq = 800 * (1 - t * 0.7) val = math.sin(2 * math.pi * freq * (i / sample_rate)) env = 1 - t sample = int(val * env * 12000) buffer.append(sample) elif sound_type == "hit": # Choque: Golpe sordo de baja frecuencia duration = 0.15 total_samples = int(sample_rate * duration) for i in range(total_samples): t = i / total_samples freq = 150 * (1 - t * 0.5) noise = (random.random() * 2 - 1) * 0.3 val = math.sin(2 * math.pi * freq * (i / sample_rate)) + noise env = (1 - t) ** 2 sample = int(val * env * 15000) buffer.append(max(-32768, min(32767, sample))) elif sound_type == "destroy": # Destrucción: Ruido de explosión corta duration = 0.2 total_samples = int(sample_rate * duration) for i in range(total_samples): t = i / total_samples noise = random.random() * 2 - 1 env = (1 - t) ** 1.5 sample = int(noise * env * 14000) buffer.append(sample) return pygame.mixer.Sound(buffer=buffer) # Cargar sonidos sintéticos SOUND_SHOOT = generate_sound("shoot") SOUND_HIT = generate_sound("hit") SOUND_DESTROY = generate_sound("destroy") def grid_to_iso(gx, gy, gz=0): """Convierte coordenadas 3D de rejilla a coordenadas 2D de pantalla.""" iso_x = (gx - gy) * (TILE_WIDTH // 2) iso_y = (gx + gy) * (TILE_HEIGHT // 2) - (gz * 16) return iso_x, iso_y def draw_isometric_prism(surface, color, width=40, height=35): """Genera una superficie isométrica con forma de prisma o cubo.""" surf = pygame.Surface((64, 64), pygame.SRCALPHA) cx, cy = 32, 32 w2 = width // 2 h2 = width // 4 top_pts = [ (cx, cy - height), (cx + w2, cy - height + h2), (cx, cy - height + 2 * h2), (cx - w2, cy - height + h2), ] left_pts = [ (cx - w2, cy - height + h2), (cx, cy - height + 2 * h2), (cx, cy + 2 * h2), (cx - w2, cy + h2), ] right_pts = [ (cx, cy - height + 2 * h2), (cx + w2, cy - height + h2), (cx + w2, cy + h2), (cx, cy + 2 * h2), ] c_top = color c_left = tuple(max(0, c - 40) for c in color) c_right = tuple(max(0, c - 70) for c in color) pygame.draw.polygon(surf, c_top, top_pts) pygame.draw.polygon(surf, c_left, left_pts) pygame.draw.polygon(surf, c_right, right_pts) pygame.draw.polygon(surf, (20, 20, 20), top_pts, 1) pygame.draw.polygon(surf, (20, 20, 20), left_pts, 1) pygame.draw.polygon(surf, (20, 20, 20), right_pts, 1) return surf def create_placeholder_sprites(): sprites = {} surf_player = pygame.Surface((64, 64), pygame.SRCALPHA) base_pts = [(32, 48), (52, 38), (32, 28), (12, 38)] apex = (32, 8) pygame.draw.polygon( surf_player, (200, 160, 0), [base_pts[3], base_pts[0], apex] ) pygame.draw.polygon( surf_player, (255, 215, 0), [base_pts[0], base_pts[1], apex] ) pygame.draw.polygon(surf_player, (255, 235, 100), base_pts) pygame.draw.line(surf_player, (50, 50, 0), apex, base_pts[0], 2) sprites["player"] = surf_player surf_vortex = draw_isometric_prism(None, COLOR_YELLOW, width=38, height=25) pygame.draw.circle(surf_vortex, (255, 255, 200), (32, 12), 8) pygame.draw.circle(surf_vortex, (0, 0, 0), (32, 12), 8, 1) sprites["vortex"] = surf_vortex # Bloques sprites["block_white"] = draw_isometric_prism( None, COLOR_WHITE, width=40, height=25 ) sprites["block_purple"] = draw_isometric_prism( None, COLOR_PURPLE, width=40, height=25 ) # Enemigos sprites["enemy_indestructible"] = draw_isometric_prism( None, COLOR_WHITE, width=32, height=35 ) sprites["enemy_red"] = draw_isometric_prism( None, COLOR_RED, width=32, height=35 ) sprites["enemy_brown"] = draw_isometric_prism( None, COLOR_BROWN, width=32, height=35 ) sprites["enemy_orange"] = draw_isometric_prism( None, COLOR_ORANGE, width=32, height=35 ) sprites["enemy_diagonal"] = draw_isometric_prism( None, COLOR_CYAN, width=32, height=35 ) surf_bullet = pygame.Surface((32, 32), pygame.SRCALPHA) pygame.draw.circle(surf_bullet, (0, 255, 255), (16, 16), 4) sprites["bullet"] = surf_bullet return sprites class SpriteManager: def __init__(self): self.placeholders = create_placeholder_sprites() self.cache = {} def get_sprite(self, name, filename=None): if name in self.cache: return self.cache[name] if filename and os.path.exists(filename): try: img = pygame.image.load(filename).convert_alpha() self.cache[name] = img return img except Exception as e: print(f"Error cargando {filename}: {e}") self.cache[name] = self.placeholders[name] return self.cache[name] class Entity: def __init__(self, gx, gy, sprite_name, filename=None, is_solid=True): self.gx = float(gx) self.gy = float(gy) self.gz = 0 self.sprite_name = sprite_name self.filename = filename self.is_solid = is_solid def draw(self, surface, camera_offset_x, camera_offset_y, sprite_mgr): screen_x, screen_y = grid_to_iso(self.gx, self.gy, self.gz) sprite = sprite_mgr.get_sprite(self.sprite_name, self.filename) draw_x = screen_x + camera_offset_x - (sprite.get_width() // 2) draw_y = ( screen_y + camera_offset_y - sprite.get_height() + (TILE_HEIGHT // 2) ) surface.blit(sprite, (draw_x, draw_y)) class Bullet(Entity): def __init__(self, gx, gy, dir_x, dir_y): super().__init__(gx, gy, "bullet", "bullet.png", is_solid=False) self.dir_x = dir_x self.dir_y = dir_y self.speed = 0.25 self.alive = True def update(self, entities): self.gx += self.dir_x * self.speed self.gy += self.dir_y * self.speed if not (0 <= self.gx < GRID_SIZE_X and 0 <= self.gy < GRID_SIZE_Y): self.alive = False return for entity in entities: if entity != self and entity.is_solid: if ( abs(entity.gx - self.gx) < 0.5 and abs(entity.gy - self.gy) < 0.5 ): self.alive = False SOUND_HIT.play() if isinstance(entity, WhiteBlock): entity.health -= 1 elif isinstance(entity, PurpleBlock): entity.push_by_bullet(self.dir_x, self.dir_y, entities) elif isinstance(entity, Enemy) and entity.destructible: entity.take_damage() break class WhiteBlock(Entity): def __init__(self, gx, gy): super().__init__( gx, gy, "block_white", "block_white.png", is_solid=True ) self.health = 2 def push(self, dx, dy, entities): new_gx = self.gx + dx new_gy = self.gy + dy if not (0 <= new_gx < GRID_SIZE_X and 0 <= new_gy < GRID_SIZE_Y): return False for entity in entities: if entity != self and entity.is_solid: if ( abs(entity.gx - new_gx) < 0.5 and abs(entity.gy - new_gy) < 0.5 ): return False self.gx = new_gx self.gy = new_gy return True class PurpleBlock(Entity): def __init__(self, gx, gy): super().__init__( gx, gy, "block_purple", "block_purple.png", is_solid=True ) def push_by_bullet(self, dir_x, dir_y, entities): step_x = 1 if dir_x > 0.3 else (-1 if dir_x < -0.3 else 0) step_y = 1 if dir_y > 0.3 else (-1 if dir_y < -0.3 else 0) new_gx = self.gx + step_x new_gy = self.gy + step_y if not (0 <= new_gx < GRID_SIZE_X and 0 <= new_gy < GRID_SIZE_Y): return False for entity in entities: if entity != self and entity.is_solid: if ( abs(entity.gx - new_gx) < 0.6 and abs(entity.gy - new_gy) < 0.6 ): return False self.gx = new_gx self.gy = new_gy return True class Enemy(Entity): def __init__(self, gx, gy, destructible=True): sprite = "enemy_red" if destructible else "enemy_indestructible" super().__init__(gx, gy, sprite, "enemy.png", is_solid=True) self.speed = 0.04 self.direction = 1 self.destructible = destructible self.health = 3 if destructible else 999 def take_damage(self): if not self.destructible: return self.health -= 1 if self.health == 2: self.sprite_name = "enemy_brown" elif self.health == 1: self.sprite_name = "enemy_orange" def update(self, entities): target_gx = self.gx + (self.speed * self.direction) if target_gx <= 0.5 or target_gx >= GRID_SIZE_X - 0.5: self.direction *= -1 return for entity in entities: if entity != self and entity.is_solid: if ( abs(entity.gx - target_gx) < 0.6 and abs(entity.gy - self.gy) < 0.6 ): self.direction *= -1 return self.gx = target_gx class DiagonalEnemy(Enemy): def __init__(self, gx, gy): super().__init__(gx, gy, destructible=False) self.sprite_name = "enemy_diagonal" self.speed_x = 0.035 self.speed_y = 0.035 self.dir_x = 1 self.dir_y = 1 self.min_y = gy - 6 self.max_y = gy + 6 def update(self, entities): target_gx = self.gx + (self.speed_x * self.dir_x) target_gy = self.gy + (self.speed_y * self.dir_y) if target_gx <= 0.5 or target_gx >= GRID_SIZE_X - 0.5: self.dir_x *= -1 target_gx = self.gx + (self.speed_x * self.dir_x) if target_gy <= self.min_y or target_gy >= self.max_y: self.dir_y *= -1 target_gy = self.gy + (self.speed_y * self.dir_y) for entity in entities: if entity != self and entity.is_solid: if ( abs(entity.gx - target_gx) < 0.6 and abs(entity.gy - target_gy) < 0.6 ): self.dir_x *= -1 self.dir_y *= -1 return self.gx = target_gx self.gy = target_gy class Player(Entity): def __init__(self, gx, gy): super().__init__(gx, gy, "player", "player.png", is_solid=True) self.speed = 0.09 self.facing_x = 0.0 self.facing_y = -1.0 self.shoot_cooldown = 0 def update(self, keys, entities, bullets): dx, dy = 0, 0 if keys[pygame.K_LEFT] or keys[pygame.K_a]: dx -= self.speed if keys[pygame.K_RIGHT] or keys[pygame.K_d]: dx += self.speed if keys[pygame.K_UP] or keys[pygame.K_w]: dy -= self.speed if keys[pygame.K_DOWN] or keys[pygame.K_s]: dy += self.speed if dx != 0 or dy != 0: norm = math.sqrt(dx * dx + dy * dy) self.facing_x = dx / norm self.facing_y = dy / norm target_gx = self.gx + dx target_gy = self.gy + dy if 0 <= target_gx < GRID_SIZE_X and 0 <= target_gy < GRID_SIZE_Y: blocked = False for entity in entities: if entity != self and entity.is_solid: if ( abs(entity.gx - target_gx) < 0.5 and abs(entity.gy - target_gy) < 0.5 ): if isinstance(entity, WhiteBlock): push_dx = ( 0.15 if dx > 0 else (-0.15 if dx < 0 else 0) ) push_dy = ( 0.15 if dy > 0 else (-0.15 if dy < 0 else 0) ) if not entity.push(push_dx, push_dy, entities): blocked = True else: blocked = True if not blocked: self.gx = target_gx self.gy = target_gy if self.shoot_cooldown > 0: self.shoot_cooldown -= 1 if keys[pygame.K_SPACE] and self.shoot_cooldown == 0: spawn_x = self.gx + (self.facing_x * 0.7) spawn_y = self.gy + (self.facing_y * 0.7) bullets.append( Bullet(spawn_x, spawn_y, self.facing_x, self.facing_y) ) SOUND_SHOOT.play() self.shoot_cooldown = 15 class Vortex(Entity): def __init__(self, gx, gy): super().__init__(gx, gy, "vortex", "vortex.png", is_solid=True) self.speed = 0.012 self.was_blocked = False def update(self, entities): target_gy = self.gy - self.speed blocked = False for entity in entities: if entity != self and entity.is_solid: if ( abs(entity.gx - self.gx) < 0.6 and abs(entity.gy - target_gy) < 0.6 ): blocked = True break if blocked and not self.was_blocked: SOUND_HIT.play() self.was_blocked = blocked if not blocked and target_gy >= 0: self.gy = target_gy class Game: def __init__(self): self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Highway Vector - Edición Isométrica") self.clock = pygame.time.Clock() self.font = pygame.font.SysFont("monospace", 14, bold=True) self.big_font = pygame.font.SysFont("monospace", 28, bold=True) self.title_font = pygame.font.SysFont("monospace", 34, bold=True) self.sprite_mgr = SpriteManager() self.lives = 4 self.score = 0 self.won = False self.in_opening = True self.reset_level() def show_opening_screen(self): """Muestra la pantalla explicativa inicial al ejecutar el juego.""" while self.in_opening: self.clock.tick(FPS) for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() if event.type == pygame.KEYDOWN: if event.key == pygame.K_RETURN: self.in_opening = False self.screen.fill((10, 10, 20)) # Título principal title_surf = self.title_font.render( "HIGHWAY VECTOR", True, COLOR_YELLOW ) self.screen.blit( title_surf, (SCREEN_WIDTH // 2 - title_surf.get_width() // 2, 20), ) # Lista de elementos a mostrar items = [ ( "player", "Jugador: Flechas / WASD para mover, ESPACIO para disparar.", ), ("vortex", "Vortex: Objetivo llevar hasta la Zona 0."), ( "block_white", "Bloque Blanco: Se rompe con disparo y se puede empujar.", ), ( "block_purple", "Bloque Morado: No se rompe, solo se empuja con disparos.", ), ( "enemy_red", "Enemigo Rojo: Se mueve de lado a lado. Matable y bloqueable.", ), ( "enemy_indestructible", "Enemigo Blanco: Se mueve de lado a lado. Solo bloqueable.", ), ( "enemy_diagonal", "Enemigo Azul: Se mueve rebotando en diagonal. Solo bloqueable.", ), ] start_y = 80 spacing_y = 65 for idx, (sprite_key, desc_text) in enumerate(items): y_pos = start_y + idx * spacing_y # Dibujar imagen del elemento sprite = self.sprite_mgr.get_sprite(sprite_key) img_x = 60 - sprite.get_width() // 2 img_y = y_pos + 12 - sprite.get_height() // 2 self.screen.blit(sprite, (img_x, img_y)) # Dibujar texto descriptivo text_surf = self.font.render(desc_text, True, (230, 230, 230)) self.screen.blit(text_surf, (110, y_pos + 4)) # Línea divisoria discreta entre filas if idx < len(items) - 1: pygame.draw.line( self.screen, (30, 30, 50), (40, y_pos + spacing_y - 8), (SCREEN_WIDTH - 40, y_pos + spacing_y - 8), ) # Mensaje para comenzar con efecto de parpadeo suave blink = (pygame.time.get_ticks() // 400) % 2 == 0 start_color = (0, 255, 200) if blink else (0, 150, 120) start_surf = self.big_font.render( "Pulse INTRO para comenzar", True, start_color ) self.screen.blit( start_surf, (SCREEN_WIDTH // 2 - start_surf.get_width() // 2, 545), ) pygame.display.flip() def reset_level(self): self.vortex = Vortex(3.5, GRID_SIZE_Y - 5) self.player = Player(3.5, GRID_SIZE_Y - 8) self.entities = [self.vortex, self.player] self.bullets = [] self.spares = [] for i in range(self.lives - 1): spare = Entity( 3.5, GRID_SIZE_Y - 3 + (i * 1.2), "player", "player.png", is_solid=False, ) self.spares.append(spare) for y in range(10, GRID_SIZE_Y - 12): if y % 6 == 0: lane = (y * 3) % GRID_SIZE_X if (y // 6) % 2 == 0: self.entities.append(WhiteBlock(lane, y)) else: self.entities.append(PurpleBlock(lane, y)) if y % 14 == 0: self.entities.append(Enemy(2.0, y, destructible=True)) if y % 20 == 0: self.entities.append(Enemy(5.0, y, destructible=False)) if y % 25 == 0: self.entities.append(DiagonalEnemy(3.5, y)) def handle_collisions(self): for entity in self.entities: if isinstance(entity, Enemy): player_hit = ( abs(entity.gx - self.player.gx) < 0.6 and abs(entity.gy - self.player.gy) < 0.6 ) vortex_hit = ( abs(entity.gx - self.vortex.gx) < 0.7 and abs(entity.gy - self.vortex.gy) < 0.7 ) if player_hit or vortex_hit: SOUND_DESTROY.play() return True for entity in self.entities[:]: if hasattr(entity, "health") and entity.health <= 0: self.entities.remove(entity) SOUND_DESTROY.play() self.score += 200 return False def draw_hud(self): zone = max(0, int((self.vortex.gy - 10) / 4)) hud_text = f"ZONE: {zone:02d} | LIVES: {self.lives} | SCORE: {self.score:06d}" text_surface = self.font.render(hud_text, True, (255, 255, 255)) pygame.draw.rect(self.screen, (0, 0, 0), (0, 0, SCREEN_WIDTH, 40)) self.screen.blit(text_surface, (20, 10)) def run(self): if self.in_opening: self.show_opening_screen() running = True while running: self.clock.tick(FPS) for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if self.won: self.screen.fill((10, 30, 20)) title = self.big_font.render( "¡MISIÓN CUMPLIDA!", True, (100, 255, 100) ) subtitle = self.font.render( "El VORTEX ha llegado con éxito a la base.", True, (220, 255, 220), ) score_msg = self.font.render( f"Puntuación Final: {self.score:06d}", True, (255, 215, 0) ) restart_msg = self.font.render( "Pulsa R para volver a jugar", True, (200, 200, 200) ) self.screen.blit(title, (SCREEN_WIDTH // 2 - 170, 200)) self.screen.blit(subtitle, (SCREEN_WIDTH // 2 - 210, 260)) self.screen.blit(score_msg, (SCREEN_WIDTH // 2 - 130, 310)) self.screen.blit(restart_msg, (SCREEN_WIDTH // 2 - 150, 380)) pygame.display.flip() keys = pygame.key.get_pressed() if keys[pygame.K_r]: self.lives = 4 self.score = 0 self.won = False self.reset_level() continue if self.lives <= 0: self.screen.fill((0, 0, 0)) msg = self.font.render( "GAME OVER - Pulsa R para Reiniciar", True, (255, 50, 50) ) self.screen.blit( msg, (SCREEN_WIDTH // 2 - 150, SCREEN_HEIGHT // 2) ) pygame.display.flip() keys = pygame.key.get_pressed() if keys[pygame.K_r]: self.lives = 4 self.score = 0 self.won = False self.reset_level() continue keys = pygame.key.get_pressed() self.player.update(keys, self.entities, self.bullets) self.vortex.update(self.entities) if self.vortex.gy <= 10: self.won = True self.score += 5000 continue for entity in self.entities: if isinstance(entity, Enemy): entity.update(self.entities) for bullet in self.bullets[:]: bullet.update(self.entities) if not bullet.alive: self.bullets.remove(bullet) if self.handle_collisions(): self.lives -= 1 if self.lives > 0: self.reset_level() continue iso_x, iso_y = grid_to_iso(self.player.gx, self.player.gy) camera_offset_x = SCREEN_WIDTH // 2 - iso_x camera_offset_y = SCREEN_HEIGHT // 2 - iso_y + 100 self.screen.fill(COLOR_BG) for gy in range(0, GRID_SIZE_Y): for gx in range(0, GRID_SIZE_X): sx, sy = grid_to_iso(gx, gy) draw_x = sx + camera_offset_x draw_y = sy + camera_offset_y if ( -100 < draw_x < SCREEN_WIDTH + 100 and -100 < draw_y < SCREEN_HEIGHT + 100 ): pts = [ (draw_x, draw_y), ( draw_x + TILE_WIDTH // 2, draw_y + TILE_HEIGHT // 2, ), (draw_x, draw_y + TILE_HEIGHT), ( draw_x - TILE_WIDTH // 2, draw_y + TILE_HEIGHT // 2, ), ] pygame.draw.polygon(self.screen, COLOR_ROAD, pts) pygame.draw.polygon(self.screen, COLOR_GRID, pts, 1) all_drawables = self.entities + self.bullets + self.spares sorted_entities = sorted(all_drawables, key=lambda e: e.gy + e.gx) for entity in sorted_entities: entity.draw( self.screen, camera_offset_x, camera_offset_y, self.sprite_mgr ) self.draw_hud() pygame.display.flip() pygame.quit() sys.exit() if __name__ == "__main__": Game().run()