#!/usr/bin/env python
# encoding: iso-8859-15
#
#Minë. Mundo Interactivo-Narrativo en Español [Tierra-Media]
#Copyright (C) 2002  Pablo Ruiz Múzquiz
#
#
#This program is free software; you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 2 of the License, or
#(at your option) any later version.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#Fichero: Sala.py

"""Clase Sala."""

import logging
import sys
import string
import os
import cPickle
import sre

from utils import *
import Objeto
import PersonajeNoJugador
import Modulo
import FechaHoraMine
import xmlutils

log = logging.getLogger("Salas")

# Version del sistema de descripciones de sala (se incrementa cada vez que cambia
# el formato de los ficheros de desc_salas)
VERSION_DESC_SALA = '1.2'

# Version del sistema de salas (se incrementa cada vez que varian los atributos
# de la clase Sala)
VERSION_SALA = '1.2.4'

DIR_DESC_SALAS = 'desc_salas'
EXT_DESC_SALAS = '.xml'
DIR_SALAS = 'salas'
EXT_SALAS = ''


TIPOS_SALA = { "Población" : [  "calle", "callejón", "plaza",
				"portal", "pasadizo", "arco",
				"puerto", "embarcadero", "azotea" ],

	       "Edificio" : [ "salón", "habitación", "pasillo",
			      "escaleras", "vestíbulo", "cuarto sin ventanas", 
			      "sótano", "calabozo", "alcantarilla", "tejado", 
			      "balcón", "letrina" ],
	    
	       "Campo" : [ "llanura", "bosque", "claro de bosque", "colina",
			   "valle", "pantano", "páramo", "desierto" ],
	    
	       "Agua" : [ "ribera", "costa", "acantilado", "cala", "rio", 
			  "lago", "mar", "islote" ],
	    
	       "Montaña" : [ "cima", "ladera", "promontorio", "grieta", 
			     "desfiladero", "quebrada", "abismo", "cornisa" ],
	    
	       "Caverna" : [ "entrada", "cueva", "pasadizo", "sima", 
			     "rio subterráneo", "lago subterráneo"]
	      }


class Sala(object):
    """Una habitacion o sala dentro del juego.
    
    Al instanciar una sala, se crea nueva y se lee desde el fichero de descripcion de sala,
    poniendola en su estado inicial. Mas tarde, se puede salvar en un fichero de sala,
    borrarla y cargarla despues, manteniendo su estado (situacion de objetos y personajes
    contenidos, tanto PJs como PNJs).
    
    Importante: al salvar una sala se salvan también todos los objetos y todos los personajes
    contenidas en ella. El módulo "Pickle" se encarga de todo, incluyendo la resolución del bucle
    formado por el atributo "sala" de la clase Personaje.

    Los ficheros XML descriptores de salas se encuentran situados debajo del directorio
    DIR_DESC_SALAS, y dentro de otro subdirectorio con el nombre del área. Por ejemplo, el
    descriptor de la sala con id poblado_calle01 estará en DIR_DESC_SALAS/poblado/poblado-calle01.xml.

    Los ficheros de salas guardados con Pickle se encuentran todos en el directorio DIR_SALAS,
    sin que haya más subdirectorios. No tienen extensión. Por ejemplo, el fichero con la sala de
    antes guardada será DIR_SALAS/poblado-calle01.
    """
	
    # Atributos públicos de la clase

    def __leer_id(self): return self.__id
    def __set_id(self,id): self.__id = id
    
    id = property(__leer_id,__set_id,
                  doc="""(string): identifica univocamente a la sala.

		      No puede contener espacios y debe tener el formato:
		      nombre_de_area-identificador_de_sala.

                      Invariantes:
                          id != None
		      """)

    def __leer_version(self): return self.__version
    def __set_version(self,version): self.__version = version
    
    version = property(__leer_version, __set_version,
                       doc="""(string): versión del sistema de salas.

		           Invariantes:
                               version != None
		           """)

    def __leer_autor(self): return self.__autor
    def __set_autor(self,autor): self.__autor = autor
        
    autor = property(__leer_autor, __set_autor,
                     doc="""(string): nombre del autor de la descripción.

		         Invariantes:
                             autor != None
		         """)

    def __leer_area(self): return self.__area
    def __set_area(self, area): self.__area =area
    
    area = property(__leer_area, __set_area,
                    doc="""(string): nombre del area a la que pertenece la sala.
		       
		        Invariantes:
                            area != None
		        """)

    def __leer_comentario(self): return self.__comentario
    def __set_comentario(self, comentario): self.__comentario = comentario
    
    comentario = property(__leer_comentario, __set_comentario,
                          doc="""(string): comentario explicativo del fichero de sala.

			      Invariantes:
       			          comentario != None
			      """)

    def __leer_nombre(self): return self.__nombre
    def __set_nombre(self,nombre): self.__nombre = nombre
    
    nombre = property(__leer_nombre,__set_nombre,
                      doc="""(string): texto con el nombre corto de la sala,
                          tal como sale en pantalla.

			  Invariantes:
                              nombre != None
			  """)

    def __leer_aura(self): return self.__aura
    def __set_aura(self, aura): self.__aura = aura
    
    aura = property(__leer_aura, __set_aura,
                    doc="""(int): nivel de aura de la sala (0-50 = malvado,
                        50 = normal, 50-100 = bueno).

			Invariantes:
                            aura in range(0,100)
			""")

    def __leer_tipo(self): return self.__tipo
    def __set_tipo(self, tipo): self.__tipo = tipo
    
    tipo = property(__leer_tipo,__set_tipo,
                    doc="""(string): el tipo de sala.
		        """)

    def __leer_subtipo(self): return self.__subtipo
    def __set_subtipo(self, subtipo): self.__subtipo = subtipo

    subtipo = property(__leer_subtipo,__set_subtipo,
                       doc="""(string): el subtipo de sala.
		           """)

    def __leer_volumen(self): return self.__volumen
    def __set_volumen(self, volumen): self.__volumen = float(volumen)

    volumen = property(__leer_volumen,__set_volumen,
                       doc="""(float): volumen total que puede contener
                           (1 unidad = tamaño de un humano).
			   
			   Invariantes:
                               volumen > 0
			   """)

    def __leer_luz(self): return self.__luz
    def __set_luz(self, luz): self.__luz = luz

    luz = property(__leer_luz, __set_luz,
                   doc="""(int): nivel de iluminación (0 = oscuridad total,
                       100 = enfrente de Eru).

		       Invariantes:
                           luz in range(0,100)
		       """)

    def __leer_combate(self): return self.__combate
    def __set_combate(self, combate): self.__combate = combate

    combate = property(__leer_combate, __set_combate,
                       doc="""(string): si se puede combatir en esta sala ("si" o "no").

		           Invariantes:
                               combate in ["si", "no"]
		           """)

    def __leer_ocultabilidad(self): return self.__ocultabilidad
    def __set_ocultabilidad(self, ocultabilidad): self.__ocultabilidad = ocultabilidad

    ocultabilidad = property(__leer_ocultabilidad,__set_ocultabilidad,
                             doc="""(int): facilidad de ocultarse (0 = imposible, 100 = seguro).

			         Invariantes:
				     ocultabilidad in range(0,100)
				 """)

    def __leer_contadores(self): return self.__contadores

    contadores = property(__leer_contadores,
			  doc="""({string:int}): cada contador es un nombre asociado a
			      un número, que se puede acceder libremente desde guiones.

		              Invariantes:
                                  contadores != None
		              """)

    def __leer_etiquetas(self): return self.__etiquetas

    etiquetas = property(__leer_etiquetas,
			 doc="""({string:string}): cada etiqueta es un nombre asociado a
			     un valor de texto, que se puede acceder libremente desde
			     guiones.

		             Invariantes:
                                 etiquetas != None
		             """)

    def __leer_descripcion(self): return self.__descripcion

    descripcion = property(__leer_descripcion,
                           doc="""({String:any}[]): descripcion de la sala.

                               Consiste en una lista de items, y cada item es un conjunto
			       de propiedades en forma de diccionario:
				 - "dificultad" (int): dificultad para percibir
						       el item (0 = automatico,
						       100 = imposible)
				 - "texto" (string): el texto del item
				 - "personaje_mira" (Modulo): el módulo que se ejecuta cuando
				                              un personaje mira el item

			       Invariantes:
				   descripcion != None
				   len(descripcion) >= 0
				   descripcion[i]["dificultad"] in range(0,100)
				   descripcion[i]["texto"] != None
			       """)

    def __leer_salidas(self): return self.__salidas

    salidas = property(__leer_salidas,
                       doc="""({String:any}[]): lista de salidas de la sala.
		       
		           Cada salida es un diccionario:
			     - "direccion" (string): el punto cardinal (norte, sur, etc.)
			     - "dificultad" (int): dificultad para percibirla (0-100)
			     - "id" (string): el id de la sala a donde se va por ahi
			     - "descripcion" (string): lo que se ve al mirar en esa direccion
			     - "tipo_cierre" (string): el tipo de cerradura que hay (ninguno,
			                               cerradura, horario, magia)
			     - "datos_cierre" (string): información adicional sobre el cierre:
				* ninguno: vacío
				* cerradura: id de objeto llave
				* horario: rango(s) horario(s) de apertura, formato
					   hora_ini-hora_fin, separados por comas. Puede 
					   incluir un espacio y el id de la llave que abre
					   cuando está cerrada
				* magia: id de hechizo (sin implementar)
			     - "mensaje_cerrado" (string): mensaje que sale si se intenta salir
			                                   estando cerrada.
			     - "personaje_mira" (Modulo): el módulo que se ejecuta cuando un
			                                  personaje mira la salida
			     - "personaje_cruza" (Modulo): el módulo que se ejecuta cuando un
			                                   personaje intenta atravesar la salida

			    Invariantes:
				salidas != None
				len(salidas) >= 0
				salidas[i]["direccion"] != Null
				salidas[i]["dificultad"] in range(0,100)
				salidas[i]["id"] != Null
				salidas[i]["descripcion"] != Null
				salidas[i]["tipo_cierre"] in ("ninguno", "cerradura", "horario", "magia")
				salidas[i]["datos_cierre"] != Null
				salidas[i]["mensaje_cerrado"] != Null
			    """)

    def __leer_objetos(self): return self.__objetos

    objetos = property(__leer_objetos,
                       doc="""({String:any}[]): lista de objetos de la sala.
		       
		           Cada objeto es un diccionario:
			    - "id" (string): el id del objeto
			    - "probabilidad" (int): probabilidad de que reaparezca cada vez
			                            que alguien entra en la sala (0-100)
			    - "maximo" (int): maximos objetos que pueden aparecer (si es 0,
			                      no hay máximo)
			    - "dificultad" (int): dificultad para percibirlo (0-100)
			    - "cantidad" (int): cantidad inicial de objetos (es útil sobre todo
			                        para el editor de salas)
			    - "descripcion" (string): texto que describe el objeto cuando está
			                              en la sala
			    - "instancias" (Objeto[]): lista con una o más instancias del
			                               objeto real (puede estar vacía si
						       probabilidad > 0, porque es posible
						       que reaparezcan objetos más tarde)
			    - "personaje_mira" (Modulo): módulo que se ejecuta cuando un
			                                 personaje mira la sala

			    Invariantes:
			      objetos != None
			      len(objetos) >= 0
			      objetos[i]["id"] != Null
			      objetos[i]["probabilidad"] in range(0,100)
			      objetos[i]["maximo"] >= 0
			      objetos[i]["dificultad"] in range(0,100)
			      objetos[i]["cantidad"] > 0
			      objetos[i]["descripcion"] != Null
			      objetos[i]["instancias"] != Null
			    """)

    def __leer_encuentros(self): return self.__encuentros

    encuentros = property(__leer_encuentros,
                          doc="""({String:any}[]): lista de pnjs propios de la sala.

			      Cada encuentro es un diccionario:
			       - "id" (string): el id del pnj
			       - "probabilidad" (int): probabilidad de que reaparezca cada
			                               vez que alguien entra en la sala (0-100)
			       - "maximo" (int): maximos pnjs que pueden aparecer (si es 0,
			                         no hay máximo)
			       - "dificultad" (int): dificultad para percibirlo (0-100)
			       - "descripcion" (string): que aspecto tiene el personaje
			       - "cantidad" (int): cantidad inicial de pnjs (es útil sobre todo
			                           para el editor de salas)

			      Invariantes:
				  encuentros != None
				  len(encuentros) >= 0
				  encuentros[i]["id"] != Null
				  encuentros[i]["probabilidad"] in range(0,100)
				  encuentros[i]["maximo"] >= 0
				  encuentros[i]["dificultad"] in range(0,100)
				  encuentros[i]["descripcion"] != Null
			          encuentros[i]["cantidad"] > 0
			      """)

    def __leer_personajes(self): return self.__personajes

    personajes = property(__leer_personajes,
                          doc="""(Personaje{}): diccionario con los personajes que se
			      encuentran en la sala, indexado por id.

			      Invariantes:
				  personajes != None
				  personajes[i].sala == self
			      """)

    def __leer_n_jugadores(self): return self.__n_jugadores

    n_jugadores = property(__leer_n_jugadores,
                           doc="""(int): numero de personajes de tipo PersonajeJugador
                               dentro de la sala.

			       Invariantes:
				   n_jugadores >= 0
				   n_jugadores <= len(personajes)
			       """)

    # Constructor

    def __init__(self, id, version, autor, area, comentario, nombre, descripcion, salidas,
	         tipo, subtipo, volumen, luz, aura, combate, ocultabilidad, contadores,
		 etiquetas, objetos, encuentros, personajes, n_jugadores, fecha_fichero):
	"""Crea una sala nueva a partir de sus atributos.

	Requiere:
	    Los parámetros satisfacen las invariantes de los atributos.
	"""

	# Atributos privados:
	#
        # __fecha_fichero (int): fecha de la ultima modificacion del fichero de descripción
	#
	# Invariantes:
        #   __fecha_fichero >= 0

	self.__id = id
	self.__version = version 
	self.__autor = autor 
	self.__area = area 
	self.__comentario = comentario 
	self.__nombre = nombre 
	self.__descripcion = descripcion 
	self.__salidas = salidas 
	self.__objetos = objetos 
	self.__tipo = tipo 
	self.__subtipo = subtipo 
	self.__volumen = volumen 
	self.__luz = luz 
	self.__aura = aura 
	self.__combate = combate 
	self.__ocultabilidad = ocultabilidad 
	self.__contadores = contadores 
	self.__etiquetas = etiquetas 
	self.__encuentros = encuentros
	self.__personajes = personajes 
	self.__n_jugadores = n_jugadores 
	self.__fecha_fichero = fecha_fichero 


    # Métodos normales

    def entrar_personaje(self, personaje):
        """Método llamado cuando un personaje entra en la sala.
	
	  Requiere:
	    personaje != None
	    personaje not in self.personajes.values()
	    personaje.sala == self
	    self.volumen_ocupado() + personaje.volumen_total() <= self.volumen

	  Asegura:
	    personaje in self.personajes.values()
	  """

        # Actualizar la lista
        self.__personajes[personaje.id] = personaje
        
        # Actualizar el contador de PJs
	if personaje.es_jugador:
            self.__n_jugadores += 1

	# Ver si aparecen o desaparecen objetos
	for objeto in self.__objetos:
	    if objeto["probabilidad"] > 0:
		if tirar_porcentaje() <= objeto["probabilidad"]:
		    if tirar_porcentaje() <= 50:
			if len(objeto["instancias"]) > 0:
			    objeto["instancias"][0].liberar()
                            del objeto["instancias"][0]
		    else:
		        if (objeto["maximo"] == 0 or
			  len(objeto["instancias"]) < objeto["maximo"]):
			    instancia = Objeto.nuevo_xml(objeto["id"])
			    instancia.contener(self)
                            objeto["instancias"].append(instancia)

            
    def salir_personaje(self, personaje):
        """Método llamado cuando un personaje sale de la sala.
	
	Requiere:
	  personaje != None
	  personaje in self.personajes.values()

	Asegura:
	  personaje not in self.personajes.values()
	"""
        
        # Actualizar la lista
	del self.__personajes[personaje.id]
    
	# Actualizar el contador de PJs
	if personaje.es_jugador:
	    self.__n_jugadores -= 1


    def personaje_mira(self, observador):
	"""Responde al suceso de que un personaje mira la descripción de esta sala.

	Por cada item de descripcion de la sala, chequea los requisitos y realiza las acciones
	correspondientes. Luego realiza la misma operación con las salidas y los objetos.
	Al final devuelve tres listas, con todos los items de descripción, salidas y objetos
	cuyo módulo de respuesta ha devuelto un resultado de éxito.

	Requiere:
	    observador != None
	"""
	items_devueltos = []
	salidas_devueltas = []
	objetos_devueltos = []

	for item in self.__descripcion:
	    if item["personaje_mira"] == None:
		items_devueltos.append(item)
	    else:
		respuesta = item["personaje_mira"]
		result =  respuesta.ejecutar(macros_valores = {"$observador": observador,
							       "$sala": self})

		if result == None:  # Si ninguna acción acepta o cancela explícitamente el
		    result = 1      # suceso, se asume que se acepta

		if result:
		    items_devueltos.append(item)

	for salida in self.__salidas:
	    if salida["personaje_mira"] == None:
		salidas_devueltas.append(salida)
	    else:
		respuesta = salida["personaje_mira"]
		result =  respuesta.ejecutar(macros_valores = {"$observador": observador,
							       "$sala": self})

		if result == None:  # Si ninguna acción acepta o cancela explícitamente el
		    result = 1      # suceso, se asume que se acepta

		if result:
		    items_devueltos.append(salida)

	for objeto in self.__objetos:
	    if len(objeto["instancias"]) > 0:
		if objeto["personaje_mira"] == None:
		    objetos_devueltos.append(objeto)
		else:
		    respuesta = objeto["personaje_mira"]
		    result =  respuesta.ejecutar(macros_valores = {"$observador": observador,
								   "$sala": self,
								   "$objeto": objeto["instancias"][0]})

		    if result == None:  # Si ninguna acción acepta o cancela explícitamente el
			result = 1      # suceso, se asume que se acepta

		    if result:
			objetos_devueltos.append(objeto)

	return (items_devueltos, salidas_devueltas, objetos_devueltos)


    def volumen_ocupado(self):
	"""El total de volumen que ocupa toda la gente y objetos que hay dentro.
	Asegura:
	  return > 0.0 
          return <= self.volumen
	"""
	ocupado = 0.0
	for personaje in self.__personajes.values():
	    ocupado += personaje.volumen_total()

	for objeto in self.__objetos:
	    for instancia in objeto["instancias"]:
		ocupado += instancia.volumen

	return ocupado


    def buscar_personaje(self, nombre_personaje):
	"""Busca un personaje en la sala, por nombre normalizado.

	- nombre_personaje es el nombre a buscar. Se comparara con el nombre del personaje
	  mediante la funcion extraer_id

	Requiere:
	  nombre_personaje != None
	Devuelve:
	  el personaje, o None si no se ha encontrado
	"""
	nombre_normal = normalizar_id(nombre_personaje)
	personajes = [per for per in self.__personajes.values()
	                  if nombre_normal == extraer_id(per.nombre)]
	if len(personajes) == 0:
	    return None
	else:
	    return personajes[0]


    def buscar_pj(self, nombre_pj):
	"""Igual que buscar_personaje, pero busca solo dentro de los pjs."""
	nombre_normal = normalizar_id(nombre_personaje)
	personajes = [per for per in self.__personajes.values()
	                  if per.es_jugador
			  and nombre_normal == extraer_id(per.nombre)]
	if len(personajes) == 0:
	    return None
	else:
	    return personajes[0]


    def buscar_pnj(self, nombre_pnj):
	"""Igual que buscar_personaje, pero busca solo dentro de los pnjs."""
	nombre_normal = normalizar_id(nombre_personaje)
	personajes = [per for per in self.__personajes.values()
	                  if not per.es_jugador
			  and nombre_normal == extraer_id(per.nombre)]
	if len(personajes) == 0:
	    return None
	else:
	    return personajes[0]


    def objeto_en_sala(self, nombre_objeto):
	"""Comprueba si en la sala existe un objeto con el nombre indicado.

	El nombre puede ser una expresión regular simple (con * y ?, como los wilcards de DOS).

	Requiere:
	    nombre_objeto != None
	"""
	# Convertir la expresion regular de tipo ?* al estilo del módulo sre.
	nombre_objeto = "^" + nombre_objeto.replace("?", ".").replace("*", ".*") + "$"
	for objeto in self.__objetos:
	    if len(objeto["instancias"]) > 0:
		if sre.match(nombre_objeto, objeto["instancias"][0].nombre):
		    resultado = True
		    break
	else:
	    resultado = False

	return resultado


    def buscar_objeto(self, id_objeto, n_orden=1):
	"""Localiza un objeto en la sala.

	- id_objeto es el identificador del objeto. Puede haber varios objetos distintos
	  con el mismo id
	- n_orden sirve para distinguir el objeto si hay mas de uno con el mismo id

	Devuelve el indice dentro de la lista de objetos de la sala que apunta al objeto buscado.

	Lanza ExcepcionDeJuego si no se encuentra.

	Requiere:
	    id_objeto != None
	    n_orden >= 1

	Asegura:
	    return in range(0, len(self.objetos))
    	"""
	encontrados = 0
	n_objeto = -1

	for objeto in self.__objetos:
	    n_objeto = n_objeto + 1
	    if len(objeto["instancias"]) > 0:
		if normalizar_id(id_objeto) == extraer_id(objeto["descripcion"]):
		    encontrados = encontrados + 1
		    if encontrados == n_orden:
			break
		    
	if encontrados == 0:
            raise ExcepcionDeJuego("No hay en la sala ningún objeto con identificativo " +
	                           id_objeto)
	elif encontrados < n_orden:
	    raise ExcepcionDeJuego("No hay en la sala suficientes objetos con identificativo " +
	                           id_objeto + ", solo hay " + str(encontrados))

	else:
	    return n_objeto


    def acceder_a_objeto(self, n_objeto, n_instancia=1):
	"""Accede a una instancia de un objeto para trabajar con ella, pero sin sacarla
	de la sala.
	
	- n_objeto es el número de orden del objeto dentro de la sala
	- n_instancia es el número de orden de la instancia a extraer

	Devuelve la instancia elegida.

	Lanza ExcepcionDeJuego si no puede acceder a ella.

	Requiere:
	    n_objeto in range(0, len(self.objetos))
	    n_instancia >= 1

	Asegura:
	    return != None
	"""
	objeto = self.__objetos[n_objeto]
	return objeto["instancias"][n_instancia-1]


    def coger(self, n_objeto, n_instancia=1, cantidad=1):
	"""Sacar una o varias instancias de un objeto de la sala.
	
	- n_objeto es el número de orden del objeto dentro de la sala
	- n_instancia es el número de orden de la primera instancia a extraer
	- cantidad es el número de instancias a extraer

	Devuelve una lista con las instancias extraidas.

	Lanza ExcepcionDeJuego si no puede sacarlas (por ejemplo, se han pedido
	más instancias de las que hay).

	Requiere:
	    n_objeto in range(0, len(self.objetos))
	    n_instancia >= 1
	    cantidad >= 1

	Asegura:
	    return != None
	    len(return) == cantidad
	"""
	objeto = self.__objetos[n_objeto]

	if (n_instancia + cantidad - 1) > len(objeto["instancias"]):
	    raise ExcepcionDeJuego("No hay " + str(n_instancia + cantidad - 1) +
				   " instancias de ese objeto, solo hay " + 
				   str(len(objeto["instancias"])))

	# Asumimos que ningún personaje puede levantar más de 10 Tm. Es una manera
	# de representar objetos fijos al suelo, que no se pueden levantar.
	objeto["instancias"][0].activar()  # para corregir cierto fallo en algunos objetos
	if objeto["instancias"][0].peso > 10000:
	    raise ExcepcionDeJuego("Este objeto no se puede coger.")

	objetos_cogidos = []

	instancias = objeto["instancias"]
	for i in range(0, cantidad):
	    objetos_cogidos.append(instancias[n_instancia-1])
	    instancias[n_instancia-1].activar()
	    instancias[n_instancia-1].liberar()
	    del instancias[n_instancia-1]

	if len(instancias) == 0 and objeto["probabilidad"] == 0:
	    del self.__objetos[n_objeto]

	return objetos_cogidos


    def dejar(self, objetos, dificultad = 0):
	"""Soltar una o varias instancias de un objeto en la sala.

	Si dificultad != 0, el objeto queda escondido (excepto si ya había
	más instancias de ese mismo objeto anteriormente en la sala).

	Requiere:
	    objetos != None
	    len(objetos) >= 1
	    dificultad in range(0, 100)
	"""
	for objeto in objetos:
	    objeto.contener(self)
        
	for objeto in self.__objetos:
	    if objeto["id"] == objetos[0].id:
		objeto["instancias"].extend(objetos)
		break
	else:
	    self.__objetos.append({"id": objetos[0].id,
	                           "probabilidad": 0,
				   "maximo": 0,
				   "dificultad": dificultad,
				   "cantidad": len(objetos),
				   "descripcion": objetos[0].nombre,
				   "instancias": objetos,
				   "personaje_mira": None})


    def objeto_desaparece(self, objeto):
	"""Un objeto contenido en esta sala ha desaparecido de aquí.

	Hay que eliminarlo de la lista de instancias de la sala. Este método será llamado
	sólo desde el propio objeto, a través del interfaz Contenedor.
	"""
	objeto.liberar()
	for i, grupo_objetos in izip(self.__objetos):
	    instancias = grupo_objetos["instancias"]
	    for j, instancia in izip(instancias):
		if instancia == objeto:
		    del instancias[j]
		    if len(instancias) == 0 and grupo_objetos["probabilidad"] == 0:
			del self.__objetos[i]
		    break


    def objeto_aparece(self, objeto):
	"""Un objeto aparece en esta sala.

	Hay que añadirlo a la lista de instancias de la sala. Este método será llamado
	sólo desde el propio objeto, a través del interfaz Contenedor.
	"""
	objeto.contener(self)
        
	for grupo_objetos in self.__objetos:
	    if grupo_objetos["id"] == objeto.id:
		grupo_objetos["instancias"].append(objeto)
		break
	else:
	    self.__objetos.append({"id": objeto.id,
	                           "probabilidad": 0,
				   "maximo": 0,
				   "dificultad": 0,
				   "cantidad": 1,
				   "descripcion": objeto.nombre,
				   "instancias": [objeto],
				   "personaje_mira": None})


    def objeto_reemplazado(self, objeto_antiguo, objeto_nuevo):
	"""Un objeto contenido en esta sala es reemplazado por otro.

	Hay que borrar el objeto antiguo de la lista de instancias de la sala, y añadir
	el nuevo.  Este método será llamado sólo desde el propio objeto, a través del
	interfaz Contenedor.
	"""
	self.objeto_desaparece(objeto_antiguo)
	self.objeto_aparece(objeto_nuevo)


    def personaje_cruza(self, viajero, sala_destino):
	"""Responde al suceso personaje_cruza de la salida que dirige a sala_destino.

	Ejecuta el módulo correspondiente, si lo hay, y devuelve True si se ha
	aceptado el comando, o False si se ha cancelado.

        Requiere:
	    viajero != None
	    sala_destino != None
	"""
	for salida in self.__salidas:
	    if salida["id"] == sala_destino.id:
		modulo = salida["personaje_cruza"]
		
		if modulo == None:
		    
		    return True
		
		else:

		    result = modulo.ejecutar(macros_valores = {"$viajero": viajero,
							       "$sala_origen": self,
							       "$sala_destino": sala_destino})

		    if result == None:  # Si ninguna acción acepta o cancela explícitamente el
			result = True   # suceso, se asume que se acepta

		    return result


    def salida_cerrada(self, id_sala_destino):
	"""Comprueba si la salida indicada, y si lo está devuelve el id de la llave,
	y el mensaje a mostrar si no se puede entrar.

        Requiere:
	    id_sala_destino apunta a una salida que existe en la sala.

	Devuelve:
	    una tupla (id_llave, mensaje_error). La id_llave vale None si la puerta está
	    abierta, y "" si no se puede abrir.
	"""
	for salida in self.__salidas:
	    if salida["id"] == id_sala_destino:

		tipo_cierre = salida["tipo_cierre"]
		if tipo_cierre == "ninguno":

		    return (None, "")

		elif tipo_cierre == "cerradura":

		    return (salida["datos_cierre"], salida["mensaje_cerrado"])

		elif tipo_cierre == "magia":

		    return ("", salida["mensaje_cerrado"])

		elif tipo_cierre == "horario":

		    # Vamos a procesar el argumento horario, que está en la forma
		    # "horaini-horafin,horaini-horafin,... idllave"

		    # Primero, separar el id de la llave
		    aux = salida["datos_cierre"].split(" ")
		    rangos = aux[0].split(",")
		    if len(aux) > 1:
			id_llave = aux[1]
		    else:
			id_llave = ""

		    # Luego, ver si la hora actual está dentro de alguno de los rangos
		    abierto = 0
		    ahora = FechaHoraMine.ahora()
		    for rango in rangos:
			aux = rango.split("-")
			hora_ini = int(aux[0])
			hora_fin = int(aux[1])
			if ahora.hora() >= hora_ini and ahora.hora() < hora_fin:
			    abierto = 1
			    break

		    if abierto:
			return (None, "")
		    else:
			return (id_llave, salida["mensaje_cerrado"])


    def cambiar_aura(self, puntos):
	"""Modifica el valor de la propiedad aura."""
	self.__aura += puntos


    def ambiente(self, texto, rango_salas=0):
        """Ocurre algo que lo ven todos los personajes en esta sala y adyacentes.

	rango_de_salas es la distancia a la que se percibe el suceso (0 = sólo sala origen,
	1 = salas a distancia 1, etc.)
	
	Requiere:
	    texto != None
	    rango_de_salas >= 0
	"""
	sala = self
	# Ahora tengo que buscar por ahí el algoritmo de recorrido de grafos sin repeticiones,
	# con distancia limitada, pero no tengo ganas. Ya lo haré un día de estos.
	for personaje in [ p for p in sala.personajes.values() if p.controlador != None]:
	    personaje.controlador.suceso_entorno(texto)


    def crear_objeto(self, id_objeto):
	"""Aparece una instancia nueva de un objeto en la sala.

	Requiere:
	    id_objeto != None
	"""
	objeto = Objeto.nuevo_xml(id_objeto)
	self.objeto_aparece(objeto)


    def crear_pnj(self, id_pnj):
	"""Aparece un nuevo pnj en esta sala.

	Requiere:
	    id_pnj != None
	"""
	id_unico = id_pnj + '%' + PersonajeNoJugador.nuevo_id()
	log.info("Vamos a crear al PNJ " + id_unico)
	self.__personajes[id_unico] = PersonajeNoJugador.nuevo(id_unico, self)
	

    def contador(self, nombre_contador):
	"""Devuelve el valor del contador con el nombre indicado.

	Es útil para requisitos. Si no existe, devuelve None.

	Requiere:
	    nombre_contador != None
	"""
	try:
	    return self.__contadores[nombre_contador]
	except KeyError:
	    return None


    def cambiar_contador(self, nombre_contador, incr_valor):
	"""Suma la cantidad indicada al contador.

	Si el nombre del contador no existe, no hace nada.

	Requiere:
	    nombre_contador != None
	    incr_valor != None
	"""
	if self.__contadores.has_key(nombre_contador):
	    self.__contadores[nombre_contador] += incr_valor


    def crear_contador(self, nombre_contador, valor):
	"""Crea un contador nuevo, con el valor indicado.

	Si ya existía, es sustituido.

	Requiere:
	    nombre_contador != None
	    valor != None
	"""
	self.__contadores[nombre_contador] = valor


    def borrar_contador(self, nombre_contador):
	"""Destruye un contador.

	Si no existía, no hace nada.

	Requiere:
	    nombre_contador != None
	"""
	try:
	    del self.__contadores[nombre_contador]
	except KeyError:
	    pass
	

    def etiqueta(self, nombre_etiqueta):
	"""Devuelve el valor de la etiqueta con el nombre indicado.

	Es útil para requisitos. Si no existe, devuelve None.

	Requiere:
	    nombre_etiqueta != None
	"""
	try:
	    return self.__etiquetas[nombre_etiqueta]
	except KeyError:
	    return None


    def cambiar_etiqueta(self, nombre_etiqueta, valor):
	"""Asigna el valor textual indicado a la etiqueta.

	Si el nombre de la etiqueta no existe, no hace nada.

	Requiere:
	    nombre_etiqueta != None
	    valor != None
	"""
	if self.__etiquetas.has_key(nombre_etiqueta):
	    self.__etiquetas[nombre_etiqueta] = valor


    def crear_etiqueta(self, nombre_etiqueta, valor):
	"""Crea una etiqueta nueva, con el valor indicado.

	Si ya existía, es sustituida.

	Requiere:
	    nombre_etiqueta != None
	    valor != None
	"""
	self.__etiquetas[nombre_etiqueta] = valor


    def borrar_etiqueta(self, nombre_etiqueta):
	"""Destruye una etiqueta.

	Si no existía, no hace nada.

	Requiere:
	    nombre_etiqueta != None
	"""
	try:
	    del self.__etiquetas[nombre_etiqueta]
	except KeyError:
	    pass


    def activar(self):
	"""Realiza algunos chequeos periodicos siempre que alguien
	necesita esta sala.
	"""
	import Mundo

	if self.__id != Mundo.ID_SALA_LIMBO:
	    # Todos los personajes no controlados deben estar siempre en el limbo,
	    # pero es posible que por algún fallo quede alguno suelto por ahí.
	    # Si se da el caso, lo movemos al limbo.
	    for personaje in self.__personajes.values()[:]:
		if personaje.controlador == None:
		    log.warning("OJO: restaurado pj " + personaje.id + " en sala " + self.__id +
			        ". Moviendolo al limbo...")
		    personaje.liberar()
	else:
	    for personaje in self.__personajes.values()[:]:

		# En el limbo, todos los personajes deben tener la sala original
		# a donde devolverlos a la vuelta. Puede que por algún error, haya
		# alguno con eso mal. Si es así, lo enviamos a la entrada.
		if not hasattr(personaje, "_Personaje__id_sala_original") or \
		   personaje._Personaje__id_sala_original == Mundo.ID_SALA_LIMBO:
		    log.warning("OJO: personaje " + personaje.id + " en el limbo ha perdido " +
			        "la sala original. Lo mandaremos a la entrada...")
		    personaje._Personaje__id_sala_original = Mundo.ID_SALA_ENTRADA

		# Asimismo, ningún personaje puede estar siguiendo a otro
		if personaje._Personaje__anterior != None:
		    log.warning("OJO: personaje " + personaje.id + " estaba siendo seguido " +
		                "por alguien. Lo cancelamos...")
		    personaje._Personaje__anterior = None
		if personaje._Personaje__siguiente != None:
		    log.warning("OJO: personaje " + personaje.id + " estaba siguiendo a " +
			        "alguien. Lo cancelamos...")
		    personaje._Personaje__siguiente = None

	for personaje in self.__personajes.values():
	    for item in personaje.descripcion:
		if not item.has_key("personaje_mira"):
		    log.warning("OJO: personaje %s no tiene guion personaje_mira en la " 
			        "descripción. Arreglado" % personaje.id)
		    item["personaje_mira"] = None

	archivo = os.path.join(DIR_DESC_SALAS, self.__id.split('-')[0], self.__id + EXT_DESC_SALAS)
	if self.__fecha_fichero < os.path.getmtime(archivo):
	    # Si se modifica el fichero descriptor, recargarlo de nuevo
	    # pero conservando la lista de personajes y los objetos *que no
	    # sean nativos* de la sala.
	    log.info("Recargando sala " + self.__id + " desde el fichero de descripcion")

	    objetos_guardados = self.__objetos

	    self.__recargar_sala()

	    for objeto_guardado in objetos_guardados:
	        og = ( objeto_guardado["id"], objeto_guardado["probabilidad"] )
		if og not in [ (o["id"], o["probabilidad"]) for o in self.__objetos ]:
		    self.__objetos.append(objeto_guardado)


    def salvar(self):
        """Graba en un fichero de salas la sala con todo su contenido."""
        
        fichero = open(os.path.join(DIR_SALAS, self.__id + EXT_SALAS), 'w')
        cPickle.dump(self, fichero)
        fichero.close()


    # Métodos especiales

    def __setstate__(self, atributos):
        """Método llamado para restaurar los atributos provinientes de un
        pickle."""

	self.__dict__ = atributos
 
        # Verificar la versión.
	if not self.__dict__.has_key("_Sala__version"):
	    raise ValueError("Cargada sala " + self.__id + " obsoleta: sin versión" +
			     " cuando se esperaba " + VERSION_SALA)

        assert(VERSION_SALA == "1.2.4")
        while self.__version != "1.2.4":

	    # La versión 1.2.1 produce una ruptura con las anteriores ya que se migra al formato
	    # de clases de Python 2.2, y no es compatible con el anterior. Hay que borrar todas
	    # las salas, personajes y objetos.
	
            if self.__version == "1.2.1":
		# Version 1.2.1 -> 1.2.2: añadir cantidad y descripcion a los objetos, y cantidad
		# a los encuentros.
		for objeto in self.__objetos:
		    objeto["cantidad"] = len(objeto["instancias"])
		    if len(objeto["instancias"]) > 0:
			objeto["descripcion"] = objeto["instancias"][0].nombre
		    else:
			instancia = Objeto.nuevo_xml(objeto["id"])
			objeto["descripcion"] = instancia.nombre
		for encuentro in self.__encuentros:
		    encuentro["cantidad"] = 1
                self.__version = "1.2.2"
                log.info("Convertida sala " + self.__id + " a versión 1.2.2")
	    elif self.__version == "1.2.2":
		# Versión 1.2.2 -> 1.2.3: añadir suceso personaje_mira a los items de descripción,
		# a las salidas y a los objetos, y personaje_cruza a las salidas.
		for item in self.__descripcion:
		    item["personaje_mira"] = None
		for objeto in self.__objetos:
		    objeto["personaje_mira"] = None
		for salida in self.__salidas:
		    salida["personaje_mira"] = None
		    salida["personaje_cruza"] = None
                self.__version = "1.2.3"
                log.info("Convertida sala " + self.__id + " a versión 1.2.3")
	    elif self.__version == "1.2.3":
		# Versión 1.2.3 -> 1.2.4: añadir contadores y etiquetas
		self.__contadores = {}
		self.__etiquetas = {}
                self.__version = "1.2.4"
                log.info("Convertida sala " + self.__id + " a versión 1.2.4")
	    else:
		raise ValueError("Cargada sala " + self.__id + " con versión desconocida: " +
				 self.__version + " cuando se esperaba " + VERSION_SALA)


    def __recargar_sala(self):
	"""Recarga todos los datos del fichero de descripción XML."""

        (id, version, autor, area, comentario, nombre, descripcion, salidas,
	    tipo, subtipo, volumen, luz, aura, combate, ocultabilidad, contadores,
	    etiquetas, objetos, encuentros, fecha_fichero) = \
		    _parsear_fichero(self.__id)

	self.__id = id
	self.__version = version 
	self.__autor = autor 
	self.__area = area 
	self.__comentario = comentario 
	self.__nombre = nombre 
	self.__descripcion = descripcion 
	self.__salidas = salidas 
	self.__tipo = tipo 
	self.__subtipo = subtipo 
	self.__volumen = volumen 
	self.__luz = luz 
	self.__aura = aura 
	self.__combate = combate 
	self.__ocultabilidad = ocultabilidad 
	self.__contadores = contadores 
	self.__etiquetas = etiquetas 
	self.__objetos = objetos 
	self.__encuentros = encuentros
	self.__fecha_fichero = fecha_fichero 


# Funciones públicas

def cargar(id):
    """Recupera una sala desde un fichero de salas.

    Requiere:
      id != None
      id corresponde con un fichero existente

    Asegura:
      return != None
    """
    
    fichero = open(os.path.join(DIR_SALAS, id + EXT_SALAS), 'r')
    sala = cPickle.load(fichero)
    fichero.close()
    
    return sala
 
            
def nueva_xml(id):
    """Crea una sala nueva a partir de un fichero XML de descripción de sala.
    
    Requiere:
      id != None
      id corresponde con un fichero existente
    """
    (id, version, autor, area, comentario, nombre, descripcion, salidas,
	tipo, subtipo, volumen, luz, aura, combate, ocultabilidad, contadores,
	etiquetas, objetos, encuentros, fecha_fichero) = _parsear_fichero(id)

    personajes = {}
    n_jugadores = 0

    sala = Sala(id, version, autor, area, comentario, nombre, descripcion, salidas,
	        tipo, subtipo, volumen, luz, aura, combate, ocultabilidad, contadores,
		etiquetas, objetos, encuentros, personajes, n_jugadores, fecha_fichero)

    for objeto in sala.objetos:
	for i in range(objeto["cantidad"]):
	    instancia = Objeto.nuevo_xml(objeto["id"])
	    sala.objeto_aparece(instancia)
    
    for encuentro in sala.encuentros:
	for i in range(encuentro["cantidad"]):
	    id_unico = encuentro["id"] + '%' + PersonajeNoJugador.nuevo_id()
	    log.info("Vamos a crear al PNJ " + id_unico)
	    pnj = PersonajeNoJugador.nuevo_xml(id_unico, sala)
	    sala.entrar_personaje(pnj)

    return sala


def _parsear_fichero(id, crear_contenidos = True):
    """Analiza un fichero XML y devuelve toda su información.

    crear_contenidos -- indica si se deben crear los objetos y encuentros
			contenidos inicialmente en la sala (esto valdrá
			False en el editor)

    La estructura del fichero XML es:

      <sala version="xxx" autor="xxx" area="xxx" comentario="xxx">
        <id>xxx</id>
	<nombre>xxx</nombre>
	<descripcion>
	  <item>xxx
	    <NOMBRE_SUCESO>
	      ...guion...
	    </NOMBRE_SUCESO>
	    ...
	  </item>
	  ...
	</descripcion>
	<salidas>
	  <DIRECCION id="xxx" tipo_cierre="xxx" datos_cierre="xxx" mensaje_cerrado="xxx">
	    xxx
 	    <NOMBRE_SUCESO>
	      ...guion...
	    </NOMBRE_SUCESO>
	    ...
	  </DIRECCION>
	  ...
	</salidas>
	<propiedades tipo="xxx" subtipo="xxx" volumen="xxx" luz="xxx", aura="xxx",
	             combate="xxx", ocultabilidad="xxx">
	  <contador nombre="xxx" valor="xxx"/>
	  ...
	  <etiqueta nombre="xxx" valor="xxx"/>
	  ...
	</propiedades>
	<objetos>
	  <objeto id="xxx" cantidad="xxx" maximo="xxx" probabilidad="xxx"
	          dificultad="xxx">
	    xxx
	    <NOMBRE_SUCESO>
	      ...guion...
	    </NOMBRE_SUCESO>
	    ...
	  </objeto>
	  ...
	</objetos>
	<encuentros>
	  <encuentro id="xxx" cantidad="xxx" maximo="xxx" probabilidad="xxx"
	             dificultad="xxx">
	    xxx
	  </encuentro>
	  ...
	</encuentros>
      </sala>

    Requiere:
      id != None
      id corresponde con un fichero existente
    """
    archivo = os.path.join(DIR_DESC_SALAS, id.split('-')[0],
			   id + EXT_DESC_SALAS)
    fecha_fichero = os.path.getmtime(archivo)
    
    elem_sala = xmlutils.parsear_fichero(archivo)
    (version, autor, area, comentario) = _parsear_sala(id, elem_sala)

    elem_id = xmlutils.subelemento(elem_sala, 'id')
    id_fichero = _parsear_id(id, elem_id)

    elem_nombre = xmlutils.subelemento(elem_sala, 'nombre')
    nombre = _parsear_nombre(id, elem_nombre)
    
    elem_descripcion = xmlutils.subelemento(elem_sala, 'descripcion')
    descripcion = _parsear_descripcion(id, elem_descripcion, crear_contenidos)
	    
    elem_salidas = xmlutils.subelemento(elem_sala, 'salidas')
    salidas = _parsear_salidas(id, elem_salidas, crear_contenidos)

    elem_propiedades = xmlutils.subelemento(elem_sala, 'propiedades')
    (tipo, subtipo, volumen, luz, aura, combate, ocultabilidad, contadores, etiquetas) = \
        _parsear_propiedades(id, elem_propiedades)
    
    elem_objetos = xmlutils.subelemento(elem_sala, 'objetos')
    objetos = _parsear_objetos(id, elem_objetos, crear_contenidos)
    
    elem_encuentros = xmlutils.subelemento(elem_sala, 'encuentros')
    encuentros = _parsear_encuentros(id, elem_encuentros)

    return (id, version, autor, area, comentario, nombre, descripcion, salidas,
	    tipo, subtipo, volumen, luz, aura, combate, ocultabilidad, contadores,
	    etiquetas, objetos, encuentros, fecha_fichero)
		
	   
def _parsear_sala(id, elem_sala):
    """Analiza el elemento <sala>. Devuelve:
        - version
	- autor
	- area
	- comentario
    """
    
    version_desc_sala = xmlutils.atributo(elem_sala, 'version')
    if version_desc_sala != VERSION_DESC_SALA:
	raise ValueError("Version incorrecta del fichero descriptor de sala " +
		         id + ": " + version_desc_sala + " cuando se esperaba " +
			 VERSION_DESC_SALA)

    version = VERSION_SALA
    autor = xmlutils.atributo(elem_sala, 'autor')
    area = xmlutils.atributo(elem_sala, 'area')
    comentario = xmlutils.atributo_def(elem_sala, 'comentario', '')

    return (version, autor, area, comentario)

	
def _parsear_id(id, elem_id):
    """Analiza el elemento <id>. Devuelve:
        - id_fichero
    """
    id_fichero = xmlutils.contenido(elem_id)
    if id_fichero != id:
	raise ValueError("El id %s no coincide con el nombre del fichero %s" % (id, id_fichero))

    return id_fichero


def _parsear_nombre(id, elem_nombre):
    """Analiza el elemento <nombre>. Devuelve:
        - nombre
    """
    nombre = xmlutils.contenido(elem_nombre)
    return nombre


def _parsear_descripcion(id, elem_descripcion, crear_contenidos):
    """Analiza el elemento <descripcion> y todos sus subelementos. Devuelve:
        - descripcion
    """
    descripcion = []

    for elem_item in xmlutils.subelementos(elem_descripcion, 'item'):

	texto_item = xmlutils.contenido(elem_item)
	texto_item = " ".join(texto_item.split()) # elimina todos los espacios redundantes	    
	
	dif_item = xmlutils.atributo_int_def(elem_item, 'dificultad', 0,
		                             u"Error en objeto %s. La dificultad de la descripción "
				             u"tiene que ser un número." % id)
	if not dif_item in range(0, 101):
	    raise ValueError("Error en objeto %s. La dificultad de la descripción "
	                     "tiene que ser de 0 a 100." % id)

	modulo = None
	for elem_suceso in xmlutils.subelementos(elem_item):
	    suceso = xmlutils.nombre_elemento(elem_suceso)
	    if suceso == "personaje_mira":
		if crear_contenidos:
		    modulo = Modulo.nuevo_xml(elem_suceso,
					      macros_tipos = [ ("$observador", "Personaje"),
							       ("$sala", "Sala") ])
		else:
		    modulo = elem_suceso.toxml()

	descripcion.append({"dificultad": dif_item, 
		            "texto": texto_item,
			    "personaje_mira": modulo})

    return descripcion


def _parsear_salidas(id, elem_salidas, crear_contenidos):	
    """Analiza el elemento <salidas> y todos sus subelementos. Devuelve:
        - salidas
    """
    salidas = []

    for elem_salida in xmlutils.subelementos(elem_salidas):
	
	direccion_salida = xmlutils.nombre_elemento(elem_salida)  # oeste, este, norte, etc.

	dif_salida = xmlutils.atributo_int_def(elem_salida, 'dificultad', 0,
		                               u"Error en sala %s. La dificultad de la salida %s"
					       u"tiene que ser un número." % (id, direccion_salida))
	if not dif_salida in range(0, 101):
	    raise ValueError("Error en sala %s. La dificultad de la salida %s "
	                     "tiene que ser de 0 a 100." % (id, direccion_salida))

	id_salida = xmlutils.atributo(elem_salida, 'id')
	tipo_cierre_salida = xmlutils.atributo_def(elem_salida, 'tipo_cierre', 'ninguno')
	datos_cierre_salida = xmlutils.atributo_def(elem_salida, 'datos_cierre', '')
	mensaje_cerrado_salida = xmlutils.atributo_def(elem_salida, 'mensaje_cerrado',
		                                       'No puedes pasar')
	    
	desc_salida = xmlutils.contenido(elem_salida)

	modulo_mira = None
	modulo_cruza = None
	for elem_suceso in xmlutils.subelementos(elem_salida):
	    suceso = xmlutils.nombre_elemento(elem_suceso)
	    if suceso == "personaje_mira":
		if crear_contenidos:
		    modulo_mira = Modulo.nuevo_xml(elem_suceso,
						   macros_tipos = [ ("$observador", "Personaje"),
								    ("$sala", "Sala") ])
		else:
		    modulo_mira = elem_suceso.toxml()
	    elif suceso == "personaje_cruza":
		if crear_contenidos:
		    modulo_cruza = Modulo.nuevo_xml(elem_suceso,
						    macros_tipos = [ ("$viajero", "Personaje"),
								     ("$sala_origen", "Sala"),
								     ("$sala_destino", "Sala") ])
		else:
		    modulo_cruza = elem_suceso.toxml()

	salidas.append({"direccion": direccion_salida,
			"dificultad": dif_salida,
			"id": id_salida, 
			"descripcion": desc_salida,
			"tipo_cierre": tipo_cierre_salida,
			"datos_cierre": datos_cierre_salida,
			"mensaje_cerrado": mensaje_cerrado_salida,
			"personaje_mira": modulo_mira,
			"personaje_cruza": modulo_cruza})

    return salidas


def _parsear_propiedades(id, elem_propiedades):	
    """Analiza el elemento <propiedades> y todos sus subelementos. Devuelve:
        - tipo
	- subtipo
	- volumen
	- luz
	- aura
	- combate
	- ocultabilidad
	- contadores
	- etiquetas
    """
    tipo = xmlutils.atributo_def(elem_propiedades, 'tipo', 'sin_tipo')
    subtipo = xmlutils.atributo_def(elem_propiedades, 'subtipo', 'sin_subtipo')
    volumen = xmlutils.atributo_float(elem_propiedades, 'volumen',
			  u"Error en sala %s. El volumen tiene que ser un número" % id)
    luz = xmlutils.atributo_int(elem_propiedades, 'luz',
			  u"Error en sala %s. La luz tiene que ser un número" % id)
    aura = xmlutils.atributo_int_def(elem_propiedades, 'aura', 50,
			 u"Error en sala %s. El aura tiene que ser un número." % id)
    if not aura in range(0, 101):
	raise ValueError(u"Error en sala %s. El aura tiene que ser de 0 a 100." %id)
    combate = xmlutils.atributo_def(elem_propiedades, 'combate', 'si')
    ocultabilidad = xmlutils.atributo_int_def(elem_propiedades, 'ocultabilidad', 30,
			 u"Error en sala %s. La ocultabilidad tiene que ser un número." % id)
	    
    contadores = {}
    for elem_contador in xmlutils.subelementos(elem_propiedades, 'contador'):
	nombre_contador = xmlutils.atributo(elem_contador, 'nombre')
	valor_contador = xmlutils.atributo_float(elem_contador, 'valor', 
		      u"Error en sala %s. El valor del contador %s tiene que ser un número." %
		      (id, nombre_contador))
	contadores[nombre_contador] = valor_contador
    
    etiquetas = {}
    for elem_etiqueta in xmlutils.subelementos(elem_propiedades, 'etiqueta'):
	nombre_etiqueta = xmlutils.atributo(elem_etiqueta, 'nombre')
	valor_etiqueta = xmlutils.atributo(elem_etiqueta, 'valor')
	etiquetas[nombre_etiqueta] = valor_etiqueta

    return (tipo, subtipo, volumen, luz, aura, combate, ocultabilidad, contadores, etiquetas)


def _parsear_objetos(id, elem_objetos, crear_contenidos):
    """Analiza el elemento <objetos> y todos sus subelementos. Devuelve:
        - objetos
    """
    objetos = []

    for elem_objeto in xmlutils.subelementos(elem_objetos, 'objeto'):
    
	id_objeto = xmlutils.atributo(elem_objeto, 'id')
	cantidad_objeto = xmlutils.atributo_int_def(elem_objeto, 'cantidad', 1, 
		         u"Error en sala %s. La cantidad del objeto %s tiene que ser un número" %
			 (id, id_objeto))
	if cantidad_objeto < 0:
	    raise ValueError(u"Error en sala %s. La cantidad del objeto %s tiene que ser "
	                     u"un número mayor o igual que cero." % (id, id_objeto))
	maximo_objeto = xmlutils.atributo_int_def(elem_objeto, 'maximo', cantidad_objeto, 
		         u"Error en sala %s. La cantidad máxima del objeto %s tiene que ser "
			 u"un número" % (id, id_objeto))
	if maximo_objeto < 1:
	    raise ValueError(u"Error en sala %s. La cantidad máxima del objeto %s tiene que "
	                     u"ser un número mayor que cero." % (id, id_objeto))
	probabilidad_objeto = xmlutils.atributo_int_def(elem_objeto, 'probabilidad', 0, 
		         u"Error en sala %s. La probabilidad del objeto %s tiene que ser "
			 u"un número" % (id, id_objeto))
	if not probabilidad_objeto in range(0, 101):
	    raise ValueError(u"Error en sala %s. La probabilidad del objeto %s tiene que "
	                     u"ser de 0 a 100." % (id, id_objeto))
	dificultad_objeto = xmlutils.atributo_int_def(elem_objeto, 'dificultad', 0, 
		         u"Error en sala %s. La dificultad del objeto %s tiene que ser "
			 u"un número" % (id, id_objeto))
	if not dificultad_objeto in range(0, 101):
	    raise ValueError(u"Error en sala %s. La dificultad del objeto %s tiene que "
	                     u"ser de 0 a 100." % (id, id_objeto))

	descripcion_objeto = xmlutils.contenido(elem_objeto)
	descripcion_objeto= " ".join(descripcion_objeto.split()) # elimina todos los espacios redundantes
    
	instancias = []

	modulo = None
	for elem_suceso in xmlutils.subelementos(elem_objeto):
	    suceso = xmlutils.nombre_elemento(elem_suceso)
	    if suceso == "personaje_mira":
		if crear_contenidos:
		    modulo = Modulo.nuevo_xml(elem_suceso,
					      macros_tipos = [ ("$observador", "Personaje"),
							       ("$sala", "Sala") ])
		else:
		    modulo = elem_suceso.toxml()

	objetos.append({"id": id_objeto,
			"probabilidad": probabilidad_objeto,
			"maximo": maximo_objeto,
			"dificultad": dificultad_objeto,
			"cantidad": cantidad_objeto,
			"descripcion": descripcion_objeto,
			"instancias": instancias,
			"personaje_mira": modulo})

    return objetos
    

def _parsear_encuentros(id, elem_encuentros):
    """Analiza el elemento <encuentros> y todos sus subelementos. Devuelve:
        - encuentros
    """
    
    encuentros = []

    for elem_encuentro in xmlutils.subelementos(elem_encuentros, 'encuentro'):

	id_encuentro = xmlutils.atributo(elem_encuentro, 'id')
	cantidad_encuentro = xmlutils.atributo_int_def(elem_encuentro, 'cantidad', 1, 
		         u"Error en sala %s. La cantidad del encuentro %s tiene que ser un número" %
			 (id, id_encuentro))
	if cantidad_encuentro < 1:
	    raise ValueError(u"Error en sala %s. La cantidad del encuentro %s tiene que ser "
	                     u"un número mayor que cero." % (id, id_encuentro))
	maximo_encuentro = xmlutils.atributo_int_def(elem_encuentro, 'maximo', cantidad_encuentro, 
		         u"Error en sala %s. La cantidad máxima del encuentro %s tiene que ser "
			 u"un número" % (id, id_encuentro))
	if maximo_encuentro < 1:
	    raise ValueError(u"Error en sala %s. La cantidad máxima del encuentro %s tiene que "
	                     u"ser un número mayor que cero." % (id, id_encuentro))
	probabilidad_encuentro = xmlutils.atributo_int_def(elem_encuentro, 'probabilidad', 0, 
		         u"Error en sala %s. La probabilidad del encuentro %s tiene que ser "
			 u"un número" % (id, id_encuentro))
	if not probabilidad_encuentro in range(0, 101):
	    raise ValueError(u"Error en sala %s. La probabilidad del encuentro %s tiene que "
	                     u"ser de 0 a 100." % (id, id_encuentro))
	dificultad_encuentro = xmlutils.atributo_int_def(elem_encuentro, 'dificultad', 0, 
		         u"Error en sala %s. La dificultad del encuentro %s tiene que ser "
			 u"un número" % (id, id_encuentro))
	if not dificultad_encuentro in range(0, 101):
	    raise ValueError(u"Error en sala %s. La dificultad del encuentro %s tiene que "
	                     u"ser de 0 a 100." % (id, id_encuentro))
	
	descripcion_encuentro = xmlutils.contenido(elem_encuentro)
	descripcion_encuentro = " ".join(descripcion_encuentro .split()) # elimina todos los espacios redundantes

	encuentros.append({"id": id_encuentro,
			   "maximo": maximo_encuentro,
			   "probabilidad": probabilidad_encuentro,
			   "dificultad": dificultad_encuentro,
			   "descripcion": descripcion_encuentro,
			   "cantidad": cantidad_encuentro})    

    return encuentros

	
# Código para pruebas unitarias del módulo.

if (__name__ == '__main__'):
    print u"No hay prueba unitaria de módulo. prueba con salatester.py"
