import math 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 # Distancia total de la autopista # Paleta de Colores COLOR_BG = (15, 15, 25) COLOR_ROAD = (50, 50, 65) COLOR_GRID = (30, 30, 45) COLOR_VORTEX = (220, 50, 50) COLOR_PLAYER = (255, 220, 0) COLOR_SPARE = (180, 160, 0) COLOR_BLOCK = (180, 70, 40) COLOR_WALL = (80, 90, 110) COLOR_ENEMY = (50, 200, 100) COLOR_BULLET = (0, 255, 255) def grid_to_iso(gx, gy, gz=0): """Convierte coordenadas 3D de rejilla en coordenadas 2D en pantalla.""" iso_x = (gx - gy) * (TILE_WIDTH // 2) iso_y = (gx + gy) * (TILE_HEIGHT // 2) - (gz * 16) return iso_x, iso_y class Entity: def __init__(self, gx, gy, color, is_solid=True): self.gx = float(gx) self.gy = float(gy) self.gz = 0 self.color = color self.is_solid = is_solid self.radius = 0.4 self.height = 20 def draw(self, surface, camera_offset_x, camera_offset_y): screen_x, screen_y = grid_to_iso(self.gx, self.gy, self.gz) draw_x = screen_x + camera_offset_x draw_y = screen_y + camera_offset_y points = [ (draw_x, draw_y - self.height), ( draw_x + TILE_WIDTH // 2, draw_y - self.height + TILE_HEIGHT // 2, ), (draw_x, draw_y - self.height + TILE_HEIGHT), ( draw_x - TILE_WIDTH // 2, draw_y - self.height + TILE_HEIGHT // 2, ), ] pygame.draw.polygon(surface, self.color, points) pygame.draw.polygon(surface, (255, 255, 255), points, 2) class Bullet(Entity): def __init__(self, gx, gy, dir_x, dir_y): super().__init__(gx, gy, COLOR_BULLET, is_solid=False) self.dir_x = dir_x self.dir_y = dir_y self.speed = 0.25 self.height = 10 self.alive = True def update(self, entities): self.gx += self.dir_x * self.speed self.gy += self.dir_y * self.speed # Comprobar límites if not (0 <= self.gx < GRID_SIZE_X and 0 <= self.gy < GRID_SIZE_Y): self.alive = False return # Impacto con entidades 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, COLOR_BLOCK, is_solid=True) self.health = 2 # Se puede destruir tras 2 disparos 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, COLOR_ENEMY, is_solid=True) self.speed = 0.03 self.direction = 1 self.health = 1 def update(self, entities): target_gx = self.gx + (self.speed * self.direction) # Rebotar en los bordes de la carretera if target_gx <= 0.5 or target_gx >= GRID_SIZE_X - 0.5: self.direction *= -1 target_gx = self.gx + (self.speed * self.direction) self.gx = target_gx class Player(Entity): def __init__(self, gx, gy): super().__init__(gx, gy, COLOR_PLAYER, is_solid=True) self.speed = 0.09 self.facing_x = 0.0 self.facing_y = -1.0 # Mirando hacia adelante por defecto 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: # Actualizar la dirección en la que mira el jugador 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 # Gestión del enfriamiento de disparo if self.shoot_cooldown > 0: self.shoot_cooldown -= 1 # Disparar al pulsar la barra espaciadora if keys[pygame.K_SPACE] and self.shoot_cooldown == 0: # Generar la bala desplazada por delante del jugador para evitar la auto-colisión 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 # Cadencia de tiro class Vortex(Entity): def __init__(self, gx, gy): super().__init__(gx, gy, COLOR_VORTEX, 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 Game") self.clock = pygame.time.Clock() self.font = pygame.font.SysFont("monospace", 18, bold=True) 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 = [] # Crear escuadrón de reserva (Vidas restantes) self.spares = [] for i in range(self.lives - 1): spare = Entity( 3.5, GRID_SIZE_Y - 3 + (i * 1.2), COLOR_SPARE, is_solid=False ) self.spares.append(spare) # Generar obstáculos y enemigos en la autopista for y in range(10, GRID_SIZE_Y - 15, 6): self.entities.append(PushBlock(y % GRID_SIZE_X, y)) if y % 12 == 0: self.entities.append(Enemy(3.5, y - 2)) def handle_collisions(self): # Muerte del jugador por contacto 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 # Eliminar bloques/enemigos destruidos for entity in self.entities[:]: if hasattr(entity, "health") and entity.health <= 0: self.entities.remove(entity) self.score += 100 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 # Actualizaciones 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() # Posicionamiento 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 suelo 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) # Renderizado en orden de profundidad (Z-Sorting por Y) 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.draw_hud() pygame.display.flip() pygame.quit() sys.exit() if __name__ == "__main__": Game().run()