- Brandon Brenes Umaña
- David González Córdoba
- Felipe Ugalde Vallejos
Curso: EIF-207 Estructuras de Datos
Institución: UNA - Universidad Nacional de Costa Rica
Período: II Ciclo 2025
- Descripción del Proyecto
- Instalación y Requisitos
- Controles del Juego
- Objetivo del Juego
- Mecánicas de Juego
- Estructuras de Datos Utilizadas
- Algoritmos y Complejidad
- Fórmulas Matemáticas
- API y Sistema de Caché
- Sistema de Guardado
- Créditos y Licencia
Courier Quest es un videojuego desarrollado en Python utilizando la librería Arcade 3.3.2. El jugador controla a un repartidor en bicicleta que debe completar pedidos en una ciudad simulada, gestionando factores como tiempo de entrega, clima dinámico, resistencia física y reputación.
El proyecto implementa diversos conceptos de estructuras de datos lineales, algoritmos de ordenamiento, gestión de archivos (JSON y binarios), integración con API REST, y un sistema de ray-casting para renderizado 3D estilo Wolfenstein.
- Python: 3.8 o superior
- Sistema Operativo: Windows, macOS o Linux
- Librerías necesarias:
- arcade==3.3.2
- requests
# Clonar el repositorio git clone https://github.com/usuario/courier-quest.git cd courier-quest # Instalar dependencias pip install -r requirements.txt # Ejecutar el juego python main.py
courier-quest/
├── api/ # Cliente API y sistema de caché
├── assets/ # Recursos multimedia (música, imágenes)
├── data/ # Archivos de respaldo offline
├── game/ # Lógica del juego
├── saves/ # Partidas guardadas
├── api_cache/ # Caché de peticiones API
├── config.json # Configuración del juego
└── main.py # Punto de entrada
- W o Flecha Arriba: Avanzar hacia adelante
- S o Flecha Abajo: Retroceder
- A o Flecha Izquierda: Girar a la izquierda
- D o Flecha Derecha: Girar a la derecha
- O: Abrir/cerrar ventana de pedidos disponibles
- Flecha Arriba/Abajo: Navegar entre pedidos (en ventana de pedidos)
- A: Aceptar pedido seleccionado (en ventana de pedidos)
- C: Cancelar pedido seleccionado (en ventana de pedidos)
- I: Abrir/cerrar inventario del jugador
- Q: Seleccionar siguiente pedido en inventario
- Shift + Q: Seleccionar pedido anterior en inventario
- Tab: Cambiar orden de inventario (prioridad/fecha límite)
- U: Deshacer último movimiento/acción
- Ctrl + Z: Deshacer último movimiento/acción (alternativo)
- ESC: Pausar juego / Volver al menú anterior
- F5: Guardar partida rápida
- Alcanzar 500ドル en ganancias antes de que se agote el tiempo límite (15 minutos de juego).
- Reputación menor a 20: El jugador pierde credibilidad y no puede continuar.
- Tiempo agotado: No se alcanzó la meta de ingresos en el tiempo límite.
Los pedidos se liberan de forma escalonada cada 2 minutos (configurable). Cada pedido tiene:
- ID único: Identificador del pedido
- Punto de recogida (pickup): Coordenadas donde recoger el paquete (Se muestra en el minimap)
- Punto de entrega (dropoff): Coordenadas donde entregar el paquete (Se muestra en el minimap)
- Pago: Cantidad de dinero que se recibe al completar
- Peso: Afecta la velocidad del jugador
- Prioridad: Nivel de importancia (0 = normal, n = alta prioridad)
- Tiempo límite: Deadline para completar la entrega
La resistencia del jugador varía entre 0-100:
- Normal (>30): Velocidad completa
- Cansado (10-30): Velocidad reducida al 80%
- Exhausto (≤0): No puede moverse hasta recuperarse al 30%
Consumo de resistencia:
- Movimiento base: -0.5 por celda
- Peso extra (>3kg): -0.2 adicional por celda por cada unidad sobre 3kg
- Clima adverso:
- Lluvia/Viento: -0.1 por celda
- Tormenta: -0.3 por celda
- Calor: -0.2 por celda
Recuperación:
- Parado: +5 puntos/segundo
- En punto de descanso: +10 puntos/segundo
La reputación comienza en 70/100 y varía según las acciones del jugador:
Cambios positivos:
- Entrega a tiempo: +3
- Entrega temprana (≥20% antes del límite): +5
- Racha de 3 entregas sin penalización: +2 (bonus único)
Cambios negativos:
- Tarde ≤30s: -2
- Tarde 31-120s: -5
- Tarde >120s: -10
- Cancelar pedido aceptado: -4
- Perder/expirar paquete: -6
Efectos de reputación:
- Reputación ≥90: +5% en todos los pagos
- Reputación <20: Derrota inmediata
El clima cambia automáticamente cada 45-60 segundos usando una matriz de transición de Markov de 9 estados:
Condiciones disponibles:
- clear: Despejado (velocidad ×ばつ1.00)
- clouds: Nublado (velocidad ×ばつ0.98)
- rain_light: Llovizna (velocidad ×ばつ0.90)
- rain: Lluvia (velocidad ×ばつ0.85)
- storm: Tormenta (velocidad ×ばつ0.75)
- fog: Niebla (velocidad ×ばつ0.88)
- wind: Viento (velocidad ×ばつ0.92)
- heat: Calor (velocidad ×ばつ0.90)
- cold: Frío (velocidad ×ばつ0.92)
Las transiciones entre climas son progresivas (3-5 segundos) para que los cambios se sientan naturales mediante interpolación lineal.
El jugador puede deshacer hasta 50 pasos anteriores:
- El sistema guarda estados cada 0.5 segundos
- Cooldown de 0.3 segundos entre undos para evitar spam
- Se guarda: posición, ángulo, stats, inventario, contadores
Ubicación: game/game.py - Sistema de gestión de pedidos
Propósito: Gestión de pedidos con liberación escalonada basada en tiempo de juego
self._orders_queue: list[tuple[float, Order]] = [] # (unlock_time_sec, Order)
Operaciones:
- Inserción: O(n log n)
- Extracción del mínimo: O(1)
Justificación: Permite liberar pedidos automáticamente según tiempo transcurrido sin mantener lista completa ordenada continuamente.
Ubicación: game/inventory.py - Sistema de inventario del jugador
Propósito: Gestión de pedidos activos con capacidad limitada por peso
self.orders: List[Order] = []
Operaciones:
- Enqueue (agregar): O(1) + ordenamiento O(n log n)
- Dequeue (remover): O(n)
Justificación: Inventario FIFO con priorización dinámica mediante ordenamiento secundario por prioridad o deadline.
Ubicación: game/renderer.py - Sistema de renderizado
Propósito: Registro de posiciones de puertas en edificios
self.door_positions = set()
Operaciones:
- Inserción: O(1)
- Búsqueda: O(1)
Justificación: Verificación rápida de puertas durante ray casting, crítico para rendimiento a 60 FPS.
Ubicación: game/renderer.py
Propósito: Evitar reconstrucción del minimap en cada frame
self._minimap_cache_key = None self._minimap_shapes = None
Complejidad: O(1) para búsqueda y acceso
Ubicación: game/weather.py
Propósito: Probabilidades de cambio entre 9 condiciones climáticas
self.transition_matrix = { WeatherCondition.CLEAR: { WeatherCondition.CLEAR: 0.4, WeatherCondition.CLOUDS: 0.3, # ... } }
Complejidad: O(1) para acceso a probabilidades
Ubicación: game/audio.py
Propósito: Almacenar archivos de audio cargados
self.music_cache = {}
Complejidad: O(1) para búsqueda y recuperación
Ubicación: game/city.py
Propósito: Mapear tipos de casillas a propiedades de superficie
self.legend = { "C": {"name": "calle", "surface_weight": 1.00}, "B": {"name": "edificio", "blocked": True}, "P": {"name": "parque", "surface_weight": 0.95} }
Complejidad: O(1) para consultas de propiedades
Ubicación: game/game.py - Métricas de rendimiento
Propósito: Registro de tiempos de frame para cálculo de FPS
self.frame_times = [] # Últimos 240 frames
Operaciones: Append O(1), pop(0) O(n)
Ubicación: game/city.py
Propósito: Matriz 2D representando el mapa
self.tiles: List[List[str]] = []
Acceso: O(1) usando índices [y][x]
Ubicación: game/renderer.py
Propósito: Cache de vectores de dirección para ray casting
self._ray_dirs: List[Tuple[float, float]] = []
Complejidad: O(n) inicialización, O(1) acceso
Ubicación: game/player.py
Propósito: Almacenar estados anteriores del jugador
from collections import deque self.undo_stack: deque = deque(maxlen=self.max_undo_steps)
Operaciones:
- Append: O(1)
- Pop: O(1)
Justificación: Deque es más eficiente que lista para operaciones en ambos extremos.
Archivo: game/renderer.py
Complejidad: O(w + h) donde w = ancho del mapa, h = alto del mapa
Contexto: Detección de paredes para renderizado 3D
El algoritmo DDA recorre el grid del mapa de forma eficiente hasta encontrar una pared, evitando comprobar cada celda del mapa.
Complejidad: O(n) donde n = número de rayos
Contexto: Proyección de columnas de pared en pantalla
Cada rayo genera una columna vertical en pantalla, con merge horizontal de slices contiguos para reducir draw calls.
Complejidad: O(n ×ばつ m) donde n = rayos, m = filas de muestreo
Contexto: Texturizado del suelo con sampling espaciado
Optimización: solo se procesan filas cada floor_row_step píxeles para reducir cálculos.
Complejidad: O(k log k) donde k = número de pedidos
Contexto: Ordenar pedidos por prioridad o deadline
self.orders.sort(key=lambda x: x.priority, reverse=True) # Por prioridad self.orders.sort(key=lambda x: x.deadline) # Por deadline
Complejidad: O(p) donde p = pedidos pendientes de liberar
Contexto: Desbloquear pedidos según tiempo transcurrido
while self._orders_queue and self._orders_queue[0][0] <= elapsed: _, order = self._orders_queue.pop(0) self.pending_orders.append(order)
Complejidad: O(w ×ばつ h) donde ×ばつh = tamaño del mapa
Contexto: Encontrar edificio más cercano para posicionar puertas
Búsqueda exhaustiva con optimización de distancia Manhattan para early termination.
Complejidad: O(c) donde c = 9 condiciones climáticas
Contexto: Elegir siguiente estado climático usando probabilidades
def _select_next_condition(self) -> str: probabilities = self.transition_matrix.get(self.current_condition, {}) # Acumulación de probabilidades y selección aleatoria
Archivo: game/player.py - Método _calculate_effective_speed()
Donde:
v0: Velocidad base del jugador (3.0 celdas/seg)M_clima: Multiplicador climático (0.75 - 1.00)M_peso: Multiplicador por peso del inventarioM_rep: Multiplicador por reputación (1.03 si rep ≥90)M_resist: Multiplicador por estado de resistenciaw_superficie: Peso de la superficie (parque = 0.95, calle = 1.0)
Archivo: game/player.py
Aplicado cuando peso_total > 3 kg
Archivo: game/renderer.py - Método _cast_wall_dda()
Archivo: game/renderer.py
Archivo: game/renderer.py
Archivo: game/renderer.py
Archivo: game/renderer.py
Archivo: game/renderer.py - Método _prepare_floor_rows()
Donde:
posZ = altura_pantalla ×ばつ 0.5y: coordenada y de la fila en pantalla
Archivo: game/renderer.py - Método _render_floor()
Archivo: game/score.py - Método calculate_score()
Archivo: game/score.py
Archivo: game/score.py
Archivo: game/score.py
Archivo: game/score.py
Archivo: game/utils.py - Función lerp()
Usado para transiciones suaves de clima y colores.
Archivo: game/utils.py - Función normalize_angle()
Archivo: game/game.py - Método _distance_player_to()
Usada para verificar proximidad entre jugador y puntos de recogida/entrega.
Archivo: game/utils.py - Función clamp()
Usada para limitar valores de stamina y reputación entre 0-100.
Base URL: https://tigerds-api.kindflower-ccaf48b6.eastus.azurecontainerapps.io
- GET /city/map: Obtener datos del mapa de la ciudad
- GET /city/jobs: Obtener lista de pedidos disponibles
- GET /city/weather: Obtener información de ráfagas climáticas
Archivo: api/cache.py
El sistema implementa un caché inteligente con:
-
TTL (Time To Live) configurable por recurso:
- Mapa: 24 horas (datos estáticos)
- Pedidos: 30 minutos (actualizaciones frecuentes)
- Clima: 15 minutos (cambios dinámicos)
-
Backup automático offline: Todos los datos de la API se guardan en
/data/para modo sin conexión. -
Limpieza automática: El caché se limpia cuando excede 100MB o contiene entradas expiradas.
Flujo de peticiones:
- Verificar conexión a Internet
- Si online: Petición a API → Guardar en caché → Guardar backup offline
- Si offline: Cargar desde caché → Si no existe, cargar desde backup
Archivo: game/SaveManager.py
El juego utiliza formato binario (pickle) para guardar partidas:
- Archivo principal:
saves/savegame.sav - Backup automático:
saves/savegame_backup.sav - Autosave:
saves/autosave.sav
- Jugador: Posición, ángulo, stamina, reputación, ganancias
- Ciudad: Dimensiones, tiles del mapa, meta de ingresos
- Pedidos: Estado de todos los pedidos (activos, completados, cancelados)
- Estadísticas: Tiempo jugado, entregas completadas, tiempo restante
- Inventario: Lista de pedidos aceptados con todos sus atributos
- Guardado rápido (F5): Guarda en cualquier momento durante el juego
- Guardado desde menú de pausa: Opción "Guardar Partida"
- Carga desde menú principal: Detecta automáticamente si existe partida guardada
- Sistema de respaldo: Tres niveles de seguridad (principal, backup, autosave)
El puntaje final se calcula al terminar una partida (victoria o derrota):
score_base = ganancias ×ばつ multiplicador_reputación
bonus_tiempo = score_base ×ばつ 0.15 (si victoria y tiempo_restante ≥ 20%)
penalizaciones = cancelaciones ×ばつ 25
score_final = max(0, score_base + bonus_tiempo - penalizaciones)
Archivo: saves/scores.json
- Guarda las 50 mejores puntuaciones
- Ordenadas de mayor a menor
- Incluye: timestamp, score, victorias/derrotas, estadísticas
- client.py: Cliente HTTP para consumir API REST
- cache.py: Sistema de caché con TTL y limpieza automática
- game.py: Bucle principal del juego, estados, lógica de victoria/derrota
- player.py: Lógica del jugador, movimiento, stats, sistema de undo
- city.py: Representación del mapa, validación de posiciones
- renderer.py: Ray casting, renderizado 3D, minimap
- weather.py: Sistema climático con cadena de Markov
- inventory.py: Gestión de inventario del jugador
- orders.py: Clases Order y OrderManager
- ordersWindow.py: Interfaz de ventana de pedidos disponibles
- audio.py: Administrador de música y efectos de sonido
- gamestate.py: Máquina de estados (menús, pausa, jugando)
- SaveManager.py: Guardado/carga de partidas en formato binario
- score.py: Sistema de puntuación y leaderboard
- settings.py: Menú de configuración
- utils.py: Funciones auxiliares (lerp, clamp, distancias)
El archivo config.json permite modificar:
- Resolución de pantalla
- Número de rayos (calidad gráfica)
- Volumen de música
- Velocidad del jugador
- Tiempo límite de partida
- Mecánicas de reputación
- Parámetros del sistema de deshacer
- Intervalos de liberación de pedidos
FOTOMENUCONFIG<
- Python 3.8+: Lenguaje de programación
- Arcade 3.3.2: Motor de juego 2D/3D
- Requests: Cliente HTTP para API REST
- JSON: Formato de datos para configuración y caché
- Pickle: Serialización binaria para guardados
Este proyecto está bajo la Licencia MIT.
MIT License
Copyright (c) 2025 David González Córdoba
Permission is hereby granted, free of charge, to any person obtaining a copy
of this