hugoruscitti/pilas

View on GitHub
pilasengine/__init__.py

Summary

Maintainability
F
4 days
Test Coverage
# -*- encoding: utf-8 -*-
# pilas engine: un motor para hacer videojuegos
#
# Copyright 2010-2014 - Hugo Ruscitti
# License: LGPLv3 (see http://www.gnu.org/licenses/lgpl.html)
#
# Website - http://www.pilas-engine.com.ar
import sys
import os
import datetime
import traceback
import random
import signal
import imp
import time
import colores

from PyQt4 import QtGui
from PyQt4 import QtCore

import configuracion
import etiquetas
import escenas
import imagenes
import actores
import utils
import fondos
import depurador
import musica
import interfaz
import sonidos
import habilidades
import comportamientos
import eventos
import controles
import pad
import watcher
import plugins
import simbolos
import datos
import fisica


import widget

VERSION = "1.4.12"


def handler(signum, frame):
    print('Terminando pilas, porque se pulsó ctrl+c.')
    sys.exit(1)

signal.signal(signal.SIGINT, handler)

class Pilas(object):
    """Representa el area de juego de pilas, el componente principal.

    El objeto pilas se inicializa cuando llamamos a la función
    ``pilasengine.iniciar()``. El objeto que se retorna es un
    objeto de esta clase.

    Internamente, este objeto será el que representa la ventana
    principal. Es es contenedor de la escena, el punto de contrucción
    de los actores y quien mantiene con "vida" el juego completo.
    """

    def __init__(self, ancho=640, alto=480, titulo='pilas-engine',
                 con_aceleracion=None, capturar_errores=True,
                 habilitar_mensajes_log=False, x=None, y=None,
                 modo_test=False,
                 pantalla_completa=False, cargar_plugins=False):
        """Inicializa el area de juego con una configuración inicial."""

        self.configuracion = configuracion.Configuracion()
        self.habilitar_mensajes_log(habilitar_mensajes_log)
        self._iniciado_desde_asistente = False
        self.texto_avisar_anterior = None
        self.modo_test = modo_test
        self._audio_inicializado = False

        # Archivo que se observa para hacer livecoding. Esta
        # variable toma valor cuando se llama a la función
        # "pilas.reiniciar_si_cambia(archivo)"
        self.archivo_a_observar = None


        self.log("Iniciando pilas con los parametros", str({"ancho": ancho,
                                                            "alto": alto,
                                                            "titulo": titulo,
                                                            "con_aceleracion": con_aceleracion,
                                                            "capturar_errores": capturar_errores,
                                                            "habilitar_mensajes_log": habilitar_mensajes_log,
                                                            "x": x,
                                                            "y": y}))
        if QtGui.QApplication.instance():
            self.app = QtGui.QApplication.instance()
            self._necesita_ejecutar_loop = False
            self.log("Obteniendo instancia a la aplicacion QT (no se re-genero el objeto de aplicacion)")
        else:
            self.app = QtGui.QApplication(sys.argv)
            self._necesita_ejecutar_loop = True
            self.log("Creando un objeto de aplicacion QT (porque no estaba inicializado)")

        self.widget = None
        self.reiniciar(ancho, alto, titulo, con_aceleracion,
                       habilitar_mensajes_log, x, y, capturar_errores, pantalla_completa)

        if self.configuracion.audio_habilitado():
            self.log("El sistema de audio esta habilitado desde la configuración")
            self._inicializar_audio()
        else:
            self.log("Evitando inicializar el sistema de audio (deshabilitado desde configuración)")

        # Solo re-define el icono cuando se usa pygame, porque
        # sino pygame pone su icono en la ventana.
        if self.configuracion.audio_habilitado():
            self._definir_icono_de_ventana()

        if cargar_plugins:
            self.complementos = plugins.Complementos(self)
        else:
            self.complementos = []

        self._usar_esc_para_alternar_pantalla_completa = True

    def debe_alternar_pantalla_completa_con_esc(self):
        return self._usar_esc_para_alternar_pantalla_completa

    def deshabilitar_alternado_de_pantalla_completa_con_esc(self, deshabilitar):
        self._usar_esc_para_alternar_pantalla_completa = not deshabilitar

    def _definir_icono_de_ventana(self):
        self.log("Definiendo el icono de la ventana")
        try:
            import pygame
            try:
                img = pygame.image.load(self.obtener_ruta_al_recurso('icono.ico'))
                pygame.display.set_icon(img)
            except pygame.error:
                pass
        except ImportError:
            self.log("Imposible cambiar el icono, parece que pygame no esta instalado...")
            pass


    def _inicializar_audio(self):
        self._audio_inicializado = True
        self.log("Inicializando el sistema de audio con pygame")
        import pygame
        pygame.init()
        pygame.mixer.init()

    def forzar_habilitacion_de_audio(self):
        if self._audio_inicializado:
            print("El audio ya ha sido inicializado")
        else:
            self._inicializar_audio()
            self.configuracion.definir_audio_habilitado(True)

    def reiniciar(self, ancho=640, alto=480, titulo='pilas-engine',
                  con_aceleracion=None, habilitar_mensajes_log=False,
                  x=None, y=None, capturar_errores=True,
                  pantalla_completa=False):
        """Genera nuevamente la ventana del videojuego."""

        # Si no especifica usar aceleracion de video, toma la
        # preferencia desde el archivo de configuración.
        if con_aceleracion == None:
            con_aceleracion = self.configuracion.aceleracion_habilitada()
            self.log("No se especificó aceleración de video, así que se adopta la preferencia desde la configuración: con_aceleracion=" + str(con_aceleracion))
        else:
            self.log("Se usa el parametro aceleracion=" + str(con_aceleracion))

        self.habilitar_mensajes_log(habilitar_mensajes_log)
        self.log("Iniciando pilas con una ventana de ", ancho, "x", alto)
        self.log("Reiniciando pilas con los parametros", str({"ancho": ancho,
                                                    "alto": alto,
                                                    "titulo": titulo,
                                                    "con_aceleracion": con_aceleracion,
                                                    "capturar_errores": capturar_errores,
                                                    "habilitar_mensajes_log": habilitar_mensajes_log,
                                                    "x": x,
                                                    "y": y}))
        self.actores = actores.Actores(self)
        self.actores.eliminar_actores_personalizados()
        self.eventos = eventos.Eventos(self)
        self.evento = self.eventos
        self.datos = datos.Datos(self)

        self.controles = controles.Controles(self)
        self.simbolos = simbolos.Simbolos(self)

        if not getattr(self, 'escenas', None):
            self.escenas = escenas.Escenas(self)

        self.escenas.eliminar_escenas_personalizadas()
        self.imagenes = imagenes.Imagenes(self)
        self.utils = utils.Utils(self)
        self.fondos = fondos.Fondos(self)
        self.colores = colores
        self.interfaz = interfaz.Interfaz(self)
        self._capturar_errores = capturar_errores

        if not getattr(self, 'depurador', None):
            self.depurador = depurador.Depurador(self)

        #if not self.configuracion.audio_habilitado():
        #    print "Nota: Iniciando con el sistema de audio deshabitado."

        self.musica = musica.Musica(self)
        self.sonidos = sonidos.Sonidos(self)

        if self.configuracion.pad_habilitado():
            self.pad = pad.Pad(self)
        else:
            self.pad = pad.PadDeshabilitado(self)

        self.habilidades = habilidades.Habilidades()

        es_reinicio = self.widget is not None

        if es_reinicio:
            self.log("Es un reinicio real (ya existia el objeto widget)")
        else:
            self.log("El reinicio se hace por primera vez (es una inicializacion en realidad)")

        if self._iniciado_desde_asistente and es_reinicio:
            parent = self._eliminar_el_anterior_widget()

        if con_aceleracion:
            self.log("Creando el widget canvas con aceleracion de video")
            self.widget = widget.WidgetConAceleracion(self, titulo, ancho, alto,
                                                      self._capturar_errores)
        else:
            self.log("Creando el widget canvas SIN aceleracion de video")
            self.widget = widget.WidgetSinAceleracion(self, titulo, ancho, alto,
                                                      self._capturar_errores)

        if self._iniciado_desde_asistente and es_reinicio:
            self._vincular_el_nuevo_widget(parent)

        self.widget.pantalla_completa = pantalla_completa

        self.escenas.Normal()

        self.comportamientos = comportamientos.Comportamientos()
        self._x = x
        self._y = y

    def esta_en_pantalla_completa(self):
        return self.widget.pantalla_completa

    def ancho(self):
        return self.widget.width()

    def alto(self):
        return self.widget.height()


    def reiniciar_si_cambia(self, archivo):
        """Regista un archivo para hacer livecoding.

        Livecoding es un modo de pilas que se reinicia automáticamente
        si el archivo indicado cambia. Esto de termina programar
        mas rápido y prototipar con mayor fluidez."""

        if not archivo:
            return

        if self.archivo_a_observar:
            raise Exception("Ya se estaba observando un archivo, imposible aceptar esta orden.")

        self.archivo_a_observar = archivo
        self.watcher = watcher.Watcher(archivo, callback=self._reiniciar_pilas_para_livecoding)

    def _reiniciar_pilas_para_livecoding(self):
        """Calback que se ejecuta cuando se detecta modificación de un archivo observado."""
        f = open(self.archivo_a_observar, 'rt')
        contenido = f.read()
        f.close()

        print "%s - Reiniciando" % (time.strftime("%H:%m:%S"))

        geometry = self.widget.geometry()

        scope = {'pilas': self, '__file__': None}
        contenido = self._modificar_codigo_para_reiniciar(contenido)

        try:
            exec(contenido, scope, scope)
        except Exception, e:
            self.procesar_error(e)

        self.widget.setGeometry(geometry)
        self.widget.show()

    def procesar_error(self, e):
        titulo = repr(e)
        descripcion = traceback.format_exc(e)
        escena = self.escenas.Error(titulo, descripcion)
        return escena

    def _modificar_codigo_para_reiniciar(self, contenido):
        import re
        contenido = re.sub('coding\s*:\s*', '', contenido)
        contenido = contenido.replace('pilas = pilasengine.iniciar', 'pilas.reiniciar')
        contenido = contenido.replace('pilas.ejecutar', '#pilas.ejecutar')

        for x in contenido.split('\n'):
            if 'import ' in x and not 'import pilasengine' in x and not 'from ' in x:
                modulo = x.split(' ')[1]
                contenido = contenido.replace(x, x + '\n' + 'reload(' + modulo + ')\n')

            if "__file__" in x:
                contenido = contenido.replace(x, "# livecoding: " + x + "\n")

        return contenido

    def cerrar(self):
        self._eliminar_el_anterior_widget()

    def definir_escena(self, escena):
        self.escenas.definir_escena(escena)

    def cambiar_escena(self, escena):
        self.definir_escena(escena)

    def definir_iniciado_desde_asistente(self, estado):
        self._iniciado_desde_asistente = estado

    def _eliminar_el_anterior_widget(self):
        """Quita de la ventana el widget utilizado anteriorente.

        Este método se suele utilizar cuando se cambia de resolución
        de pantalla o se re-inicia pilas completamente."""

        self.log("Eliminando el widget de canvas principal")
        parent = self.widget.parent()

        if parent:
            parent.removeWidget(self.widget)

        self.widget.setParent(None)
        self.widget.deleteLater()
        self.widget = None

        return parent

    def _vincular_el_nuevo_widget(self, parent):
        """Comienza a mostrar el nuevo widget en pantalla.

        Este método se utiliza para mostrar nuevamente el area de
        juego después de haber cambiado de resolución o reiniciado
        pilas."""
        if parent:
            self.log("Vinculando el widget canvas al layout")
            parent.addWidget(self.widget)
            parent.setCurrentWidget(self.widget)


    def usa_aceleracion(self):
        """Informa si está habilitado el modo aceleración de video."""
        return self.widget.usa_aceleracion_de_video()

    def obtener_widget(self):
        """Retorna el widget en donde se dibuja el juego completo.

        El 'widget' es un componente de la interfaz de usuario, que
        en nuestro caso contiene toda el area de juego."""
        return self.widget

    def obtener_centro_fisico(self):
        """Retorna el centro de la ventana en pixels."""
        return self.widget.obtener_centro_fisico()

    def obtener_coordenada_de_pantalla_relativa(self, x, y):
        """Convierte una coordenada pantalla en una coordenada de común.

        Las coordenadas comunes son las que utilizamos en pilas, donde
        el centro de pantalla es el punto (0, 0). Las coordenadas
        de pantalla, en cambio, son las que tienen como punto (0, 0)
        la esquina superir izquierda de la pantalla.
        """
        dx, dy = self.widget.obtener_centro_fisico()
        return (x - dx, (y - dy) * -1)

    def obtener_coordenada_de_pantalla_absoluta(self, x, y):
        """Convierte una coordenada común en una coordenada de pantalla.

        Las coordenadas comunes son las que utilizamos en pilas, donde
        el centro de pantalla es el punto (0, 0). Las coordenadas
        de pantalla, en cambio, son las que tienen como punto (0, 0)
        la esquina superir izquierda de la pantalla.
        """
        dx, dy = self.widget.obtener_centro_fisico()
        return (x + dx, dy - y)

    def obtener_area(self):
        """Retorna el tamaño real de la ventana."""
        return self.widget.obtener_area()

    def habilitar_mensajes_log(self, estado):
        self._imprimir_mensajes_log = estado

    def obtener_escena_actual(self):
        """Retorna la escena actual."""
        return self.escenas.obtener_escena_actual()

    def escena_actual(self):
        """Retorna la escena actual."""
        return self.obtener_escena_actual()

    def realizar_actualizacion_logica(self):
        """Realiza la etapa de actualización lógica."""
        self.escenas.realizar_actualizacion_logica()

    def realizar_actualizacion_logica_en_modo_pausa(self):
        self.escenas.realizar_actualizacion_logica_en_modo_pausa()

    def forzar_actualizacion_de_interpolaciones(self):
        self.escenas.forzar_actualizacion_de_interpolaciones()

    def simular_actualizacion_logica(self):
        """Realiza un TICK o actualización forzada de lógica.

        Este método es casi interno, se llama desde la batería de tests,
        donde no podemos ejecutar pilas de manera tradicional, con una
        ventana o una llamada a pilas.ejecutar.
        """
        self.escenas.simular_actualizacion_logica()

    def realizar_dibujado(self, painter):
        """Realiza la etapa de actualización gráfica."""
        try:
            self.escenas.realizar_dibujado(painter)
            self.depurador.realizar_dibujado(painter)
        except Exception, e:
            if self._capturar_errores:
                self.log("Capturando un error: %s", e)
                self.depurador.desactivar_todos_los_modos()
                e = sys.exc_info()
                titulo = str(e[1])
                descripcion = traceback.format_exception(e[0], e[1], e[2])
                descripcion = '\n'.join(descripcion)
                _ = self.escenas.Error(titulo, descripcion)
                traceback.print_exc()
            else:
                self.log("Capturando un error: %s", e)
                traceback.print_exc()
                sys.exit(1)

    def log(self, *mensaje):
        """Muestra un mensaje de prueba sobre la consola."""

        if self._imprimir_mensajes_log:
            hora = datetime.datetime.now().strftime("%H:%M:%S")
            mensaje = map(lambda x: str(x), mensaje)
            print(":: %s :: %s " % (hora, " ".join(mensaje)))

    def obtener_ruta_al_recurso(self, ruta):
        """Busca la ruta a un archivo de recursos.

        Los archivos de recursos (como las imagenes) se buscan en varios
        directorios (ver docstring de image.load), así que esta
        función intentará dar con el archivo en cuestión.

        :param ruta: Ruta al archivo (recurso) a inspeccionar.
        """
        self.log("Buscando ruta al recurso:", ruta)
        return utils.obtener_ruta_al_recurso(ruta)

    def ejecutar(self):
        """Muestra la ventana y mantiene el programa en ejecución."""
        if not self._iniciado_desde_asistente:
            if self.widget.pantalla_completa:
                self.widget.showFullScreen()
            else:
                self.widget.show()
                self.widget.raise_()

                self.widget.definir_tamano_real()

                if self._x and self._y:
                    self.widget.move(self._x, self._y)
                else:
                    self.widget.centrar()

        # Inicializa el bucle de pyqt solo si es necesario.
        if self._necesita_ejecutar_loop:
            self.app.exec_()

    def terminar(self):
        self.widget.close()

    def avisar(self, texto):
        if self.texto_avisar_anterior and not self.texto_avisar_anterior.esta_eliminado():
            self.texto_avisar_anterior.eliminar()

        self.texto_avisar_anterior = self.actores.TextoInferior(texto)

    def ocultar_puntero_del_mouse(self):
        self.widget.setCursor(QtGui.QCursor(QtCore.Qt.BlankCursor))

    def mostrar_puntero_del_mouse(self):
        self.widget.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))

    def obtener_posicion_del_mouse(self):
        return (self.widget.mouse_x, self.widget.mouse_y)

    def obtener_camara(self):
        return self.escena_actual().camara

    def obtener_tareas(self):
        return self.escena_actual().tareas

    def obtener_fisica(self):
        return self.escena_actual().fisica

    def obtener_colisiones(self):
        return self.escena_actual().colisiones

    def obtener_control(self):
        return self.escena_actual().control

    def obtener_actores_en(self, x, y):
        return self.escena_actual().obtener_actores_en(x, y)

    def obtener_actores(self):
        return self.escena_actual().obtener_actores()

    def azar(self, a, b):
        """Retorna un número al azar entre `a` y `b`"""
        return random.randint(a, b)

    def ver(self, objeto):
        """Imprime en pantalla el codigo fuente asociado a un objeto.

        :param objeto: El objeto que se quiere inspeccionar.
        """
        import inspect

        try:
            codigo = inspect.getsource(objeto.__class__)
        except TypeError:
            try:
                codigo = inspect.getsource(objeto)
            except TypeError:
                codigo = "<< imposible inspeccionar código para mostrar >>"

        print codigo

    def definir_pantalla_completa(self, estado):
        if estado:
            self.widget.definir_modo_pantalla_completa()
        else:
            self.widget.definir_modo_ventana()

    def obtener_actor_por_indice(self, indice):
        return self.escena._actores.obtener_actores()[indice]

    def deshabilitar_musica(self, estado=True):
        if estado:
            self.musica.deshabilitar()
        else:
            self.musica.habilitar()

    def deshabilitar_sonido(self, estado=True):
        if estado:
            self.sonidos.deshabilitar()
        else:
            self.sonidos.habilitar()


    control = property(obtener_control, doc="Obtiene el modulo de control")
    tareas = property(obtener_tareas, doc="Obtiene el modulo de tareas")
    camara = property(obtener_camara, doc="Cámara de la escena actual")
    escena = property(obtener_escena_actual, doc="Escena actual")
    fisica = property(obtener_fisica, doc="Retorna el componente fisica")
    colisiones = property(obtener_colisiones, doc="Retorna las colisiones de la escena")


def iniciar(ancho=640, alto=480, titulo='pilas-engine', capturar_errores=True,
            habilitar_mensajes_log=False, con_aceleracion=None, x=None, y=None,
            modo_test=False,
            pantalla_completa=False, cargar_plugins=False):
    """
    Inicia la ventana principal del juego con algunos detalles de funcionamiento.

    Ejemplo de invocación:

        >>> pilas.iniciar(ancho=320, alto=240)

    .. image:: ../../pilas/data/manual/imagenes/iniciar_320_240.png

    :rtype: Pilas

    Parámetros:

    :ancho: el tamaño en pixels para la ventana.
    :alto: el tamaño en pixels para la ventana.
    :titulo: el titulo a mostrar en la ventana.
    :modo_test: subrimer todo mensaje de error por consola, pensado para el lanzador de test automático.
    :capturar_errores: True indica que los errores se tienen que mostrar en la
                       ventana de pilas. En caso de poner False los errores
                       se muestran en consola.
    :habilitar_mensajes_log: Muestra cada operación que hace pilas en consola.
    :con_aceleracion: Indica si se habilita o no la aceleracion de video. Por omisión se trata de obtener la preferencia desde la configuración de pilas.
    :cargar_plugins: Parametro de tipo booleano. Si es True, se cargan todos los plugins que se encuentren dentro del directorio
                     de plugins de pilas.
    """

    pilas = Pilas(ancho=ancho, alto=alto, titulo=titulo,
                  capturar_errores=capturar_errores, x=x, y=y,
                  habilitar_mensajes_log=habilitar_mensajes_log,
                  con_aceleracion=con_aceleracion,
                  pantalla_completa=pantalla_completa,
                  modo_test=modo_test,
                  cargar_plugins=cargar_plugins)
    return pilas


def abrir_asistente():
    import asistente
    return asistente.abrir()

def abrir_manual():
    import manual
    return manual.abrir()

def abrir_api():
    import api
    return api.abrir()

def abrir_configuracion(parent=None):
    import configuracion
    return configuracion.abrir(parent)

def abrir_interprete():
    import interprete
    return interprete.abrir()

def abrir_editor():
    import interprete
    return interprete.abrir_editor()

def abrir_script_con_livereload(archivo):
    import interprete
    ruta = os.path.dirname(archivo)
    ruta = os.path.abspath(ruta)

    return interprete.abrir_script_con_livereload(archivo)




def abrir_script(archivo):

    def terminar_con_error(mensaje):
        _ = QtGui.QApplication(sys.argv)
        error = QtGui.QMessageBox()
        error.critical(None, "Uh, algo anda mal...", mensaje)
        sys.exit(1)

    def ejecutar_archivo(nombre):
        try:
            imp.load_source("__main__", nombre)
        except Exception, e:
            terminar_con_error("Error al ejecutar " + nombre + ":\n" + str(e))

    ruta_absoluta_al_archivo = os.path.abspath(archivo)
    ruta = os.path.dirname(ruta_absoluta_al_archivo)
    os.chdir(ruta)
    ejecutar_archivo(ruta_absoluta_al_archivo)