Para gestionar la selección de portadas en la aplicación, implementamos una interfaz gráfica con Tkinter que permite a los usuarios establecer y previsualizar imágenes de portada.
import tkinter as tk
from PIL import Image, ImageTk
class GestorPortada:
def __init__(self, ventana_principal, gestor_imagenes, administrador_portadas):
self.raiz = ventana_principal
self.gestor_img = gestor_imagenes
self.admin_portadas = administrador_portadas
self.crear_interfaz()
def crear_interfaz(self):
# Botón para establecer portada
btn_establecer = tk.Button(
self.raiz,
text="Establecer como portada",
command=self.establecer_portada_actual
)
btn_establecer.pack(pady=10)
# Botón para previsualizar
btn_vista_previa = tk.Button(
self.raiz,
text="Ver portada actual",
command=self.mostrar_vista_previa
)
btn_vista_previa.pack(pady=10)
def establecer_portada_actual(self):
imagen_actual = self.gestor_img.obtener_imagen_actual()
if self.admin_portadas.guardar_portada(imagen_actual):
tk.messagebox.showinfo("Éxito", "Portada configurada correctamente")
else:
tk.messagebox.showerror("Error", "No se pudo establecer la portada")
def mostrar_vista_previa(self):
ventana_previa = tk.Toplevel(self.raiz)
ventana_previa.title("Vista previa de portada")
ventana_previa.geometry("450x350")
ruta_portada = self.admin_portadas.obtener_ruta_portada()
if ruta_portada:
img = Image.open(ruta_portada)
img = img.resize((400, 300), Image.Resampling.LANCZOS)
imagen_tk = ImageTk.PhotoImage(img)
etiqueta = tk.Label(ventana_previa, image=imagen_tk)
etiqueta.imagen = imagen_tk
etiqueta.pack(expand=True, fill=tk.BOTH)
Configuración del Motor Pygame
Configuramos los parámetros fundamentales del juego y preparamos el entorno gráfico.
import pygame
import random
import sys
import time
import math
from pygame.locals import *
# Inicialización del motor
pygame.init()
pygame.font.init()
# Dimensiones de la ventana
ANCHO_VENTANA = 1080
ALTO_VENTANA = 1920
ANCHO_JUEGO = ANCHO_VENTANA
# Tamaños de entidades
ANCHO_JUGADOR, ALTO_JUGADOR = 60, 60
ANCHO_ENEMIGO, ALTO_ENEMIGO = 50, 50
# Posición inicial de enemigos
POS_INICIAL_ENEMIGO = -ALTO_ENEMIGO - 20
# Configuración de controles táctiles
ZONA_MUERTA_TOQUE = 30
VELOCIDAD_MOVIMIENTO = 17
# Ruta de fuente
ruta_fuente = pygame.font.match_font('simsun')
Estructura de Datos para Entidades
Implementamos una lista enlazada simple para gestionar dinámicamente los proyectiles y enemigos.
class Entidad:
def __init__(self):
self.pos_x = 0
self.pos_y = 0
self.siguiente = None
# Listas principales
lista_proyectiles = Entidad()
nave_jugador = Entidad()
enemigo_alpha = Entidad()
enemigo_beta = Entidad()
# Temporizadores del juego
temporizador_disparo = 0
temporizador_enemigo1 = 0
temporizador_enemigo2 = 0
# Estado del juego
puntuacion = 0
destruidos = 0
salud_jugador = 1
juego_terminado = False
# Sistema de partículas
rastros_proyectiles = []
# Animación de texto
animacion_preparado = {
'inicio': 0,
'duracion': 2000,
'pos_y_base': ALTO_VENTANA // 2,
'rebote': 0,
'opacidad': 255,
'tamaño': 180
}
Gestión de Recursos Gráficos
Creamos superficies de coler para los elementos del juego y configuramos el sistema de partículas.
def crear_superficie_color(ancho, alto, color):
superficie = pygame.Surface((ancho, alto), pygame.SRCALPHA)
superficie.fill(color)
return superficie
# Sprites del juego
sprite_jugador = crear_superficie_color(ANCHO_JUGADOR, ALTO_JUGADOR, (0, 180, 255))
sprite_enemigo = crear_superficie_color(ANCHO_ENEMIGO, ALTO_ENEMIGO, (255, 50, 50))
sprite_fondo = crear_superficie_color(ANCHO_VENTANA, ALTO_VENTANA, (0, 0, 30))
# Ventana del juego
ventana = pygame.display.set_mode((ANCHO_VENTANA, ALTO_VENTANA))
pygame.display.set_caption("Arcade Espacial - Efectos de Partículas")
reloj = pygame.time.Clock()
def obtener_fuente(tamaño):
try:
return pygame.font.Font(ruta_fuente, tamaño)
except:
return pygame.font.SysFont(None, tamaño)
Sistema de Partículas para Proyectiles
Generamos efectos visuales de estela para los disparos del jugador.
def generar_estela_proyectil(x, y):
for _ in range(random.randint(3, 6)):
desplazamiento_x = random.randint(-4, 4)
desplazamiento_y = random.randint(18, 32)
tamano = random.randint(2, 5)
transparencia = random.randint(120, 220)
color_particula = (255, random.randint(170, 255), 20, transparencia)
rastros_proyectiles.append({
'coord_x': x + desplazamiento_x,
'coord_y': y + desplazamiento_y,
'dimension': tamano,
'color': color_particula,
'vida_util': random.randint(8, 15),
'vel_y': random.randint(-3, 0)
})
def actualizar_dibujar_estelas():
for estela in rastros_proyectiles[:]:
estela['coord_y'] += estela['vel_y']
estela['vida_util'] -= 1
r, g, b, a = estela['color']
nuevo_alpha = max(0, a - 12)
estela['color'] = (r, g, b, nuevo_alpha)
if estela['vida_util'] <= 0 or nuevo_alpha == 0:
rastros_proyectiles.remove(estela)
else:
pygame.draw.circle(
ventana,
estela['color'],
(int(estela['coord_x']), int(estela['coord_y'])),
estela['dimension']
)
Inteligencia Artificial de Enemigos
Implementamos un sistema de esquiva básico para que los enmeigos intenten evitar los proyectiles.
def calcular_esquiva(enemigo_x, enemigo_y, ancho_enem, alto_enem):
proyectil_actual = lista_proyectiles.siguiente
rango_deteccion = 550
direccion_movimiento = 0
while proyectil_actual is not None:
distancia_vertical = enemigo_y - proyectil_actual.pos_y
if 0 < distancia_vertical < rango_deteccion:
if (proyectil_actual.pos_x < enemigo_x + ancho_enem and
proyectil_actual.pos_x + 10 > enemigo_x):
velocidad_proyectil = 12
tiempo_impacto = max(1, distancia_vertical // velocidad_proyectil)
if tiempo_impacto <= 2:
if proyectil_actual.pos_x + 12 < enemigo_x + ancho_enem // 2:
direccion_movimiento = 1
else:
direccion_movimiento = -1
break
proyectil_actual = proyectil_actual.siguiente
return direccion_movimiento
Inicialización del Juego
Configuramos las posiciones iniciales de todas las entidades y mostramos la animación de inicio.
def inicializar_posiciones():
nave_jugador.pos_x = ANCHO_VENTANA // 2 - ANCHO_JUGADOR // 2
nave_jugador.pos_y = ALTO_VENTANA - ALTO_JUGADOR - 40
nave_jugador.siguiente = None
lista_proyectiles.siguiente = None
enemigo_alpha.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
enemigo_alpha.pos_y = POS_INICIAL_ENEMIGO
enemigo_alpha.siguiente = None
enemigo_beta.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
enemigo_beta.pos_y = POS_INICIAL_ENEMIGO - 180
enemigo_beta.siguiente = None
def dibujar_texto_preparado():
global animacion_preparado
tiempo_actual = pygame.time.get_ticks()
transcurrido = tiempo_actual - animacion_preparado['inicio']
progreso = min(1.0, transcurrido / animacion_preparado['duracion'])
# Efecto de rebote
fuerza_rebote = 35
velocidad_rebote = 5.5
animacion_preparado['rebote'] = fuerza_rebote * abs(
math.sin(progreso * velocidad_rebote * math.pi)
)
# Ciclo de colores
if progreso < 0.3:
color_texto = (255, 255, 255)
elif progreso < 0.6:
color_texto = (200, 201, 255)
else:
color_texto = (254, 255, 255)
# Parpadeo de opacidad
intensidad = 1 + 0.1 * math.sin(progreso * 15)
alpha = int(255 * intensidad * (1 - (progreso * 0.3)))
fuente_principal = obtener_fuente(animacion_preparado['tamaño'])
superficie_texto = fuente_principal.render("¡Prepárate para la misión!", True, color_texto)
superficie_texto.set_alpha(alpha)
pos_y_texto = animacion_preparado['pos_y_base'] - animacion_preparado['rebote']
rect_texto = superficie_texto.get_rect(center=(ANCHO_VENTANA // 2, pos_y_texto))
ventana.blit(superficie_texto, rect_texto)
fuente_secundaria = obtener_fuente(45)
superficie_ayuda = fuente_secundaria.render("Toca la pantalla para mover tu nave", True, (255, 255, 255))
superficie_ayuda.set_alpha(alpha)
pos_y_ayuda = pos_y_texto + 140 - (animacion_preparado['rebote'] * 0.5)
rect_ayuda = superficie_ayuda.get_rect(center=(ANCHO_VENTANA // 2, pos_y_ayuda))
ventana.blit(superficie_ayuda, rect_ayuda)
def secuencia_inicio():
global temporizador_disparo, temporizador_enemigo1, temporizador_enemigo2
ventana.fill((0, 0, 0))
animacion_preparado['inicio'] = pygame.time.get_ticks()
tiempo_inicio = time.time()
while time.time() - tiempo_inicio < 2.2:
ventana.fill((0, 0, 0))
dibujar_texto_preparado()
for evento in pygame.event.get():
if evento.type == QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
reloj.tick(60)
tiempo_actual = pygame.time.get_ticks()
temporizador_disparo = tiempo_actual
temporizador_enemigo1 = tiempo_actual
temporizador_enemigo2 = tiempo_actual
Sistema de Disparos
Gestionamos la creación y movimiento de los proyectiles del jugador.
def crear_proyectil():
global temporizador_disparo
nuevo_proyectil = Entidad()
nuevo_proyectil.pos_x = nave_jugador.pos_x + ANCHO_JUGADOR // 2 - 5
nuevo_proyectil.pos_y = nave_jugador.pos_y - 30
nuevo_proyectil.siguiente = lista_proyectiles.siguiente
lista_proyectiles.siguiente = nuevo_proyectil
# Animación de puntuación
datos_puntuacion = {
'visible': False,
'valor': 0,
'inicio': 0,
'duracion': 2000,
'pos_y_base': ALTO_VENTANA // 2 - 180,
'rebote': 0
}
def mostrar_animacion_puntuacion():
global datos_puntuacion, juego_terminado
tiempo_actual = pygame.time.get_ticks()
if not datos_puntuacion['visible']:
return
transcurrido = tiempo_actual - datos_puntuacion['inicio']
progreso = min(1.0, transcurrido / datos_puntuacion['duracion'])
# Escalado de fuente
if progreso < 0.2:
tamaño_fuente = int(280 + (70 - 28) * (progreso / 0.2))
elif progreso < 0.7:
tamaño_fuente = 700
else:
tamaño_fuente = int(70 - (70 - 40) * ((progreso - 0.7) / 0.3))
# Efecto de rebote
fuerza_rebote = 25
velocidad_rebote = 9
datos_puntuacion['rebote'] = fuerza_rebote * abs(
math.sin(progreso * velocidad_rebote * math.pi)
)
# Transición de colores
if progreso < 0.3:
color_puntos = (255, 255, 0)
elif progreso < 0.6:
color_puntos = (255, 200, 0)
else:
color_puntos = (255, 220, 100)
# Control de opacidad
if juego_terminado:
alpha = 255
elif progreso < 0.3:
alpha = 255
else:
alpha = int(255 * (1 - (progreso - 0.3) / 0.7))
fuente_puntos = obtener_fuente(tamaño_fuente)
superficie_puntos = fuente_puntos.render(f"+{datos_puntuacion['valor']}", True, color_puntos)
superficie_puntos.set_alpha(alpha)
pos_y_puntos = datos_puntuacion['pos_y_base'] - datos_puntuacion['rebote']
rect_puntos = superficie_puntos.get_rect(center=(ANCHO_VENTANA // 2, pos_y_puntos))
ventana.blit(superficie_puntos, rect_puntos)
# Efecto de brillo
superficie_brillo = fuente_puntos.render(f"+{datos_puntuacion['valor']}", True, (255, 255, 150))
superficie_brillo.set_alpha(int(alpha * 0.4))
for dx, dy in [(-3, -3), (-3, 0), (-3, 3), (0, -3), (0, 3), (3, -3), (3, 0), (3, 3)]:
ventana.blit(superficie_brillo,
(rect_puntos.centerx - superficie_brillo.get_width() // 2 + dx,
rect_puntos.centery - superficie_brillo.get_height() // 2 + dy))
Bucle Principle del Juego
El núcleo del juego donde se procesan todos los sistemas.
def bucle_principal():
global destruidos, salud_jugador, desplazamiento_fondo, temporizador_disparo
global temporizador_enemigo1, temporizador_enemigo2, juego_terminado
inicializar_posiciones()
secuencia_inicio()
ejecutando = True
toque_activo = False
posicion_toque = (0, 0)
desplazamiento_fondo = 0
while ejecutando:
tiempo_actual = pygame.time.get_ticks()
ventana.fill((0, 0, 0))
# Sistema de fondo deslizable
desplazamiento_fondo += 2
if desplazamiento_fondo >= ALTO_VENTANA:
desplazamiento_fondo = 0
ventana.blit(sprite_fondo, (0, desplazamiento_fondo - ALTO_VENTANA))
ventana.blit(sprite_fondo, (0, desplazamiento_fondo))
# Renderizado de entidades
ventana.blit(sprite_jugador, (nave_jugador.pos_x, nave_jugador.pos_y))
ventana.blit(sprite_enemigo, (enemigo_alpha.pos_x, enemigo_alpha.pos_y))
ventana.blit(sprite_enemigo, (enemigo_beta.pos_x, enemigo_beta.pos_y))
# Generación de disparos
if tiempo_actual - temporizador_disparo >= 580 and not juego_terminado:
crear_proyectil()
temporizador_disparo = tiempo_actual
# Procesamiento de proyectiles
proyectil_actual = lista_proyectiles.siguiente
anterior = lista_proyectiles
while proyectil_actual is not None:
# Dibujar proyectil
pygame.draw.rect(ventana, (255, 255, 100),
(proyectil_actual.pos_x, proyectil_actual.pos_y, 10, 32))
pygame.draw.rect(ventana, (255, 255, 255),
(proyectil_actual.pos_x, proyectil_actual.pos_y, 10, 6))
proyectil_actual.pos_y -= 12
generar_estela_proyectil(proyectil_actual.pos_x, proyectil_actual.pos_y)
# Detección de impactos
impacto = False
valor_impacto = 0
if not juego_terminado:
# Colisión con enemigo alpha
if (proyectil_actual.pos_y - 8 <= enemigo_alpha.pos_y + ALTO_ENEMIGO and
proyectil_actual.pos_y + 32 + 8 >= enemigo_alpha.pos_y and
proyectil_actual.pos_x - 4 <= enemigo_alpha.pos_x + ANCHO_ENEMIGO and
proyectil_actual.pos_x + 10 + 4 >= enemigo_alpha.pos_x):
impacto = True
valor_impacto = 1
enemigo_alpha.pos_y = POS_INICIAL_ENEMIGO
enemigo_alpha.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
destruidos += valor_impacto
# Colisión con enemigo beta
elif (proyectil_actual.pos_y - 8 <= enemigo_beta.pos_y + ALTO_ENEMIGO and
proyectil_actual.pos_y + 32 + 8 >= enemigo_beta.pos_y and
proyectil_actual.pos_x - 4 <= enemigo_beta.pos_x + ANCHO_ENEMIGO and
proyectil_actual.pos_x + 10 + 4 >= enemigo_beta.pos_x):
impacto = True
valor_impacto = 1
enemigo_beta.pos_y = POS_INICIAL_ENEMIGO - 180
enemigo_beta.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
destruidos += valor_impacto
# Activar animación de puntuación
if impacto and valor_impacto > 0:
datos_puntuacion['visible'] = True
datos_puntuacion['valor'] = destruidos
datos_puntuacion['inicio'] = tiempo_actual
# Eliminar proyectil
if impacto or proyectil_actual.pos_y < -60:
anterior.siguiente = proyectil_actual.siguiente
del proyectil_actual
proyectil_actual = anterior.siguiente
else:
anterior = proyectil_actual
proyectil_actual = proyectil_actual.siguiente
# Actualizar efectos visuales
actualizar_dibujar_estelas()
mostrar_animacion_puntuacion()
# Movimiento de enemigo alpha con IA
if tiempo_actual - temporizador_enemigo1 >= 55 and not juego_terminado:
direccion = calcular_esquiva(enemigo_alpha.pos_x, enemigo_alpha.pos_y,
ANCHO_ENEMIGO, ALTO_ENEMIGO)
if direccion == 1 and enemigo_alpha.pos_x < ANCHO_JUEGO - ANCHO_ENEMIGO:
enemigo_alpha.pos_x += 28
elif direccion == -1 and enemigo_alpha.pos_x > 0:
enemigo_alpha.pos_x -= 7
if enemigo_alpha.pos_y < ALTO_VENTANA:
enemigo_alpha.pos_y += 4
else:
enemigo_alpha.pos_y = POS_INICIAL_ENEMIGO
enemigo_alpha.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
temporizador_enemigo1 = tiempo_actual
# Movimiento de enemigo beta con IA
if tiempo_actual - temporizador_enemigo2 >= 75 and not juego_terminado:
direccion = calcular_esquiva(enemigo_beta.pos_x, enemigo_beta.pos_y,
ANCHO_ENEMIGO, ALTO_ENEMIGO)
if direccion == 1 and enemigo_beta.pos_x < ANCHO_JUEGO - ANCHO_ENEMIGO:
enemigo_beta.pos_x += 18
elif direccion == -1 and enemigo_beta.pos_x > 0:
enemigo_beta.pos_x -= 6
if enemigo_beta.pos_y < ALTO_VENTANA:
enemigo_beta.pos_y += 2
else:
enemigo_beta.pos_y = POS_INICIAL_ENEMIGO - 180
enemigo_beta.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
temporizador_enemigo2 = tiempo_actual
# Detección de colisiones jugador-enemigo
colision_alpha = (nave_jugador.pos_x + ANCHO_JUGADOR >= enemigo_alpha.pos_x and
nave_jugador.pos_x <= enemigo_alpha.pos_x + ANCHO_ENEMIGO and
nave_jugador.pos_y + ALTO_JUGADOR >= enemigo_alpha.pos_y and
nave_jugador.pos_y <= enemigo_alpha.pos_y + ALTO_ENEMIGO)
colision_beta = (nave_jugador.pos_x + ANCHO_JUGADOR >= enemigo_beta.pos_x and
nave_jugador.pos_x <= enemigo_beta.pos_x + ANCHO_ENEMIGO and
nave_jugador.pos_y + ALTO_JUGADOR >= enemigo_beta.pos_y and
nave_jugador.pos_y <= enemigo_beta.pos_y + ALTO_ENEMIGO)
if colision_alpha or colision_beta:
salud_jugador = 0
# Pantalla de Game Over
if salud_jugador == 0:
fuente_gameover = obtener_fuente(350)
texto_gameover = fuente_gameover.render("¡JUEGO TERMINADO!", True, (255, 0, 0))
rect_gameover = texto_gameover.get_rect(center=(ANCHO_VENTANA // 2, ALTO_VENTANA // 2))
ventana.blit(texto_gameover, rect_gameover)
fuente_puntuacion = obtener_fuente(160)
texto_puntuacion = fuente_puntuacion.render(f"Puntuación Final: {destruidos}",
True, (255, 255, 255))
rect_puntuacion = texto_puntuacion.get_rect(
center=(ANCHO_VENTANA // 2, ALTO_VENTANA // 2 + 240))
ventana.blit(texto_puntuacion, rect_puntuacion)
pygame.display.update()
time.sleep(3)
ejecutando = False
# Manejo de eventos
for evento in pygame.event.get():
if evento.type == QUIT:
ejecutando = False
elif evento.type == MOUSEBUTTONDOWN:
toque_activo = True
posicion_toque = evento.pos
elif evento.type == MOUSEMOTION:
if toque_activo:
posicion_toque = evento.pos
elif evento.type == MOUSEBUTTONUP:
toque_activo = False
# Control táctil
if toque_activo:
x_toque, y_toque = posicion_toque
objetivo_x = x_toque - ANCHO_JUGADOR // 2
objetivo_y = y_toque - ALTO_JUGADOR // 2
objetivo_x = max(0, min(objetivo_x, ANCHO_VENTANA - ANCHO_JUGADOR))
objetivo_y = max(0, min(objetivo_y, ALTO_VENTANA - ALTO_JUGADOR))
if abs(nave_jugador.pos_x - objetivo_x) > ZONA_MUERTA_TOQUE:
nave_jugador.pos_x += VELOCIDAD_MOVIMIENTO if objetivo_x > nave_jugador.pos_x else -VELOCIDAD_MOVIMIENTO
if abs(nave_jugador.pos_y - objetivo_y) > ZONA_MUERTA_TOQUE:
nave_jugador.pos_y += VELOCIDAD_MOVIMIENTO if objetivo_y > nave_jugador.pos_y else -VELOCIDAD_MOVIMIENTO
# Controles de teclado (alternativo)
teclas = pygame.key.get_pressed()
if teclas[K_UP] and nave_jugador.pos_y > 0:
nave_jugador.pos_y -= 5
if teclas[K_DOWN] and nave_jugador.pos_y < ALTO_VENTANA - ALTO_JUGADOR:
nave_jugador.pos_y += 5
if teclas[K_LEFT] and nave_jugador.pos_x > 0:
nave_jugador.pos_x -= 5
if teclas[K_RIGHT] and nave_jugador.pos_x < ANCHO_VENTANA - ANCHO_JUGADOR:
nave_jugador.pos_x += 5
pygame.display.update()
reloj.tick(60)
pygame.quit()
sys.exit(0)
# Punto de entrada
if __name__ == "__main__":
bucle_principal()