import math import os import sys import pygame pygame.init() # 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 de Respaldo / Suelo COLOR_BG = (15, 15, 25) COLOR_ROAD = (50, 50, 65) COLOR_GRID = (30, 30, 45) def grid_to_iso(gx, gy, gz=0): """Convierte coordenadas de la cuadrícula a coordenadas isométricas de pantalla.""" iso_x = (gx - gy) * (TILE_WIDTH // 2) iso_y = (gx + gy) * (TILE_HEIGHT // 2) - (gz * 16) return iso_x, iso_y def create_placeholder_sprites(): """Genera sprites vectoriales en memoria si no existen archivos PNG locales.""" sprites = {} # 1. Jugador (Lasertron) surf_player = pygame.Surface((64, 64), pygame.SRCALPHA) pygame.draw.polygon( surf_player, (255, 200, 0), [(32, 8), (56, 24), (32, 56), (8, 24)], ) pygame.draw.polygon( surf_player, (255, 255, 255), [(32, 8), (56, 24), (32, 56), (8, 24)], 2, ) pygame.draw.circle(surf_player, (0, 255, 255), (32, 28), 6) sprites["player"] = surf_player # 2. VORTEX surf_vortex = pygame.Surface((64, 64), pygame.SRCALPHA) pygame.draw.polygon( surf_vortex, (220, 50, 50), [(32, 4), (60, 20), (52, 52), (12, 52)], ) pygame.draw.polygon( surf_vortex, (255, 255, 255), [(32, 4), (60, 20), (52, 52), (12, 52)], 2, ) pygame.draw.circle(surf_vortex, (255, 255, 0), (32, 28), 10) sprites["vortex"] = surf_vortex # 3. Bloque empujable surf_block = pygame.Surface((64, 64), pygame.SRCALPHA) pygame.draw.polygon( surf_block, (180, 70, 40), [(32, 12), (58, 26), (32, 40), (6, 26)], ) pygame.draw.polygon( surf_block, (120, 40, 20), [(6, 26), (32, 40), (32, 58), (6, 44)], ) pygame.draw.polygon( surf_block, (150, 50, 30), [(58, 26), (32, 40), (32, 58), (58, 44)], ) sprites["block"] = surf_block # 4. Enemigo Alienígena (Drone) surf_enemy = pygame.Surface((64, 64), pygame.SRCALPHA) pygame.draw.polygon( surf_enemy, (50, 200, 100), [(32, 10), (54, 28), (32, 46), (10, 28)], ) pygame.draw.polygon( surf_enemy, (255, 255, 255), [(32, 10), (54, 28), (32, 46), (10, 28)], 2, ) pygame.draw.circle(surf_enemy, (255, 0, 0), (32, 28), 5) # Ojo rojo sprites["enemy"] = surf_enemy # 5. Bala surf_bullet = pygame.Surface((32, 32), pygame.SRCALPHA) pygame.draw.circle(surf_bullet, (0, 255, 255), (16, 16), 5) pygame.draw.circle(surf_bullet, (255, 255, 255), (16, 16), 2) sprites["bullet"] = surf_bullet return sprites class SpriteManager: def __init__(self): self.placeholders = create_placeholder_sprites() self.cache = {} def get_sprite(self, name, filename): if name in self.cache: return self.cache[name] if 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 al cargar {filename}, usando respaldo vectorial: {e}" ) self.cache[name] = self.placeholders[name] return self.cache[name] class Entity: def __init__(self, gx, gy, sprite_name, filename, 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) # Centrar la imagen con respecto a la casilla isométrica draw_x = screen_x + camera_offset_x - (sprite.get_width() // 2) draw_y = screen_y + camera_offset_y - (sprite.get_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 if isinstance(entity, (PushBlock, Enemy)): entity.health -= 1 break class PushBlock(Entity): def __init__(self, gx, gy): super().__init__(gx, gy, "block", "block.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 Enemy(Entity): def __init__(self, gx, gy): super().__init__(gx, gy, "enemy", "enemy.png", is_solid=True) self.speed = 0.04 self.direction = 1 self.health = 1 def update(self, entities): # Calcular la posición objetiva horizontal target_gx = self.gx + (self.speed * self.direction) # 1. Rebotar si llega a los límites físicos de la carretera if target_gx <= 0.5 or target_gx >= GRID_SIZE_X - 0.5: self.direction *= -1 return # 2. Comprobar colisión con bloques u otras entidades sólidas blocked = False for entity in entities: if entity != self and entity.is_solid: # Comprobar la distancia con el bloque en la rejilla if ( abs(entity.gx - target_gx) < 0.6 and abs(entity.gy - self.gy) < 0.6 ): blocked = True break if blocked: # Si se topa con un bloque empujable u otro obstáculo, invierte su rumbo self.direction *= -1 else: # Si el camino está despejado, avanza self.gx = target_gx 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, PushBlock): 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) ) 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 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 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 Encounter - Full Sprites & Enemies") self.clock = pygame.time.Clock() self.font = pygame.font.SysFont("monospace", 18, bold=True) self.sprite_mgr = SpriteManager() self.lives = 4 self.score = 0 self.reset_level() 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 = [] # Escuadrón de reserva (vidas) 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) # Generar bloques y enemigos alternados a lo largo de la autopista for y in range(10, GRID_SIZE_Y - 15, 5): # Agregar bloques self.entities.append(PushBlock(y % GRID_SIZE_X, y)) # Agregar enemigos cada 10 casillas if y % 10 == 0: self.entities.append(Enemy(3.5, y - 2)) def handle_collisions(self): # Colisión con enemigos for entity in self.entities: if isinstance(entity, Enemy): if ( abs(entity.gx - self.player.gx) < 0.5 and abs(entity.gy - self.player.gy) < 0.5 ): self.lives -= 1 if self.lives > 0: self.reset_level() return # Limpiar entidades destruidas (bloques o enemigos con salud <= 0) for entity in self.entities[:]: if hasattr(entity, "health") and entity.health <= 0: self.entities.remove(entity) self.score += 150 def draw_hud(self): zone = int((GRID_SIZE_Y - self.vortex.gy) / 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): running = True while running: self.clock.tick(FPS) for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if self.lives <= 0: self.screen.fill((0, 0, 0)) msg = self.font.render( "GAME OVER - Press R to Restart", 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.reset_level() continue # Actualización keys = pygame.key.get_pressed() self.player.update(keys, self.entities, self.bullets) self.vortex.update(self.entities) 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) self.handle_collisions() # Desplazamiento de cámara centrado en el jugador 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 # Renderizado self.screen.fill(COLOR_BG) # Dibujar mosaico de la carretera 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) # Dibujar elementos ordenados por profundidad (Y descendente) all_drawables = self.entities + self.bullets + self.spares sorted_entities = sorted( all_drawables, key=lambda e: e.gy, reverse=True ) 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()