diff src/parpg/objects/base.py @ 0:1fd2201f5c36

Initial commit of parpg-core.
author M. George Hansen <technopolitica@gmail.com>
date Sat, 14 May 2011 01:12:35 -0700
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parpg/objects/base.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,508 @@
+#   This file is part of PARPG.
+
+#   PARPG 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 3 of the License, or
+#   (at your option) any later version.
+
+#   PARPG 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 PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+"""Containes classes defining the base properties of all interactable in-game 
+   objects (such as Carryable, Openable, etc. These are generally independent 
+   classes, which can be combined in almost any way and order. 
+
+   Some rules that should be followed when CREATING base property classes:
+   
+   1. If you want to support some custom initialization arguments, 
+      always define them as keyword ones. Only GameObject would use 
+      positional arguments.
+   2. In __init__() **ALWAYS** call the parent's __init__(**kwargs), preferably 
+      *at the end* of your __init__() (makes it easier to follow)
+   3. There should always be an attributes.append(x) call on __init__ 
+      (where X is the name of the class)
+
+   EXAMPLE:
+
+   class Openable(object):
+       def __init__ (self, is_open = True, **kwargs):
+           self.attribbutes.append("openable")
+           self.is_open = is_open
+           super(Openable,self).__init__ (**kwargs)
+        
+
+   Some rules are to be followed when USING the base classes to make composed 
+   ones:
+
+   1. The first parent should always be the base GameObject class
+   2. Base classes other than GameObject can be inherited in any order
+   3. The __init__ functoin of the composed class should always invoke the
+      parent's __init__() *before* it starts customizing any variables.
+
+   EXAMPLE:
+
+   class TinCan (GameObject, Container, Scriptable, Destructable, Carryable):
+       def __init__ (self, *args, **kwargs):
+           super(TinCan,self).__init__ (*args, **kwargs)
+           self.name = 'Tin Can'"""
+         
+class BaseObject(object):
+    """A base class that supports dynamic attributes functionality"""
+    def __init__ (self):
+        if not self.__dict__.has_key("attributes"):
+            self.attributes = []
+    
+    def trueAttr(self, attr):
+        """Method that checks if the instance has an attribute"""
+        return attr in self.attributes
+
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        state = {}
+        state["attributes"] = self.attributes
+        return state
+
+class DynamicObject (BaseObject):
+    """Class with basic attributes"""
+    def __init__ (self, name="Dynamic object", real_name=None, image=None, **kwargs):
+        """Initialise minimalistic set of data
+           @type name: String
+           @param name: Object display name
+           @type image: String or None
+           @param name: Filename of image to use in inventory"""
+        BaseObject.__init__(self)
+        self.name = name
+        self.real_name = real_name or name
+        self.image = image
+
+    def prepareStateForSaving(self, state):
+        """Prepares state for saving
+        @type state: dictionary
+        @param state: State of the object  
+        """
+        pass
+    
+    def restoreState(self, state):
+        """Restores a state from a saved state
+        @type state: dictionary
+        @param state: Saved state  
+        """
+        self.__dict__.update(state)
+
+    def __getstate__(self):
+        odict = self.__dict__.copy()
+        self.prepareStateForSaving(odict)
+        return odict
+    
+    def __setstate__(self, state):
+        self.restoreState(state)
+    
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        state = BaseObject.getStateForSaving(self)
+        state["Name"] = self.name
+        state["RealName"] = self.real_name
+        state["Image"] = self.image
+        return state
+
+class GameObject (DynamicObject):
+    """A base class to be inherited by all game objects. This must be the
+       first class (left to right) inherited by any game object."""
+    def __init__ (self, ID, gfx = None, xpos = 0.0, ypos = 0.0, map_id = None, 
+                  blocking=True, name="Generic object", real_name="Generic object", text="Item description",
+                  desc="Detailed description", **kwargs):
+        """Set the basic values that are shared by all game objects.
+           @type ID: String
+           @param ID: Unique object identifier. Must be present.
+           @type gfx: Dictionary
+           @param gfx: Dictionary with graphics for the different contexts       
+           @type coords 2-item tuple
+           @param coords: Initial coordinates of the object.
+           @type map_id: String
+           @param map_id: Identifier of the map where the object is located
+           @type blocking: Boolean
+           @param blocking: Whether the object blocks character movement
+           @type name: String
+           @param name: The display name of this object (e.g. 'Dirty crate')
+           @type text: String
+           @param text: A longer description of the item
+           @type desc: String
+           @param desc: A long description of the item that is displayed when it is examined
+           """
+        DynamicObject.__init__(self, name, real_name, **kwargs)
+        self.ID = ID
+        self.gfx = gfx or {}
+        self.X = xpos
+        self.Y = ypos
+        self.map_id = map_id
+        self.blocking = True
+        self.text = text
+        self.desc = desc
+        
+    def _getCoords(self):
+        """Get-er property function"""
+        return (self.X, self.Y)
+    
+    def _setCoords(self, coords):
+        """Set-er property function"""
+        self.X, self.Y = float(coords[0]), float (coords[1])
+        
+    coords = property (_getCoords, _setCoords, 
+        doc = "Property allowing you to get and set the object's \
+                coordinates via tuples")
+    
+    def __repr__(self):
+        """A debugging string representation of the object"""
+        return "<%s:%s>" % (self.name, self.ID)
+
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        state = super(GameObject, self).getStateForSaving()
+        state["ObjectModel"] = self.gfx
+        state["Text"] = self.text
+        state["Desc"] = self.desc
+        state["Position"] = list(self.coords)
+        return state
+
+
+class Scriptable (BaseObject):
+    """Allows objects to have predefined parpg executed on certain events"""
+    def __init__ (self, parpg = None, **kwargs):
+        """Init operation for scriptable objects
+           @type parpg: Dictionary
+           @param parpg: Dictionary where the event strings are keys. The 
+           values are 3-item tuples (function, positional_args, keyword_args)"""
+        BaseObject.__init__(self)
+        self.attributes.append("scriptable")
+        self.parpg = parpg or {}
+        
+    def runScript (self, event):
+        """Runs the script for the given event"""
+        if event in self.parpg and self.parpg[event]:
+            func, args, kwargs = self.parpg[event]
+            func (*args, **kwargs)
+            
+    def setScript (self, event, func, args = None , kwargs = None):
+        """Sets a script to be executed for the given event."""
+        args = args or {}
+        kwargs = kwargs or {}
+        self.parpg[event] = (func, args, kwargs)
+
+class Openable(DynamicObject, Scriptable):
+    """Adds open() and .close() capabilities to game objects
+       The current state is tracked by the .is_open variable"""
+    def __init__(self, is_open = True, **kwargs):
+        """Init operation for openable objects
+           @type is_open: Boolean
+           @param is_open: Keyword boolean argument sets the initial state."""
+        DynamicObject.__init__(self, **kwargs)
+        Scriptable.__init__(self, **kwargs)
+        self.attributes.append("openable")
+        self.is_open = is_open
+    
+    def open(self):
+        """Opens the object, and runs an 'onOpen' script, if present"""
+        self.is_open = True
+        try:
+            if self.trueAttr ('scriptable'):
+                self.runScript('onOpen')
+        except AttributeError :
+            pass
+            
+    def close(self):
+        """Opens the object, and runs an 'onClose' script, if present"""
+        self.is_open = False
+        try:
+            if self.trueAttr ('scriptable'):
+                self.runScript('onClose')
+        except AttributeError :
+            pass
+        
+class Lockable (Openable):
+    """Allows objects to be locked"""
+    def __init__ (self, locked = False, is_open = True, **kwargs):
+        """Init operation for lockable objects
+           @type locked: Boolean
+           @param locked: Keyword boolen argument sets the initial locked state.
+           @type is_open: Boolean
+           @param is_open: Keyword boolean argument sets the initial open state.
+                           It is ignored if locked is True -- locked objects
+                           are always closed."""
+        self.attributes.append("lockable")
+        self.locked = locked
+        if locked :
+            is_open = False
+        Openable.__init__( self, is_open, **kwargs )
+        
+    def unlock (self):
+        """Handles unlocking functionality"""
+        self.locked = False      
+        
+    def lock (self):
+        """Handles  locking functionality"""
+        self.close()
+        self.locked = True
+        
+    def open (self, *args, **kwargs):
+        """Adds a check to see if the object is unlocked before running the
+           .open() function of the parent class"""
+        if self.locked:
+            raise ValueError ("Open failed: object locked")
+        super (Lockable, self).open(*args, **kwargs)
+        
+class Carryable (DynamicObject):
+    """Allows objects to be stored in containers"""
+    def __init__ (self, weight=0.0, bulk=0.0, **kwargs):
+        DynamicObject.__init__(self, **kwargs)
+        self.attributes.append("carryable")
+        self.in_container = None
+        self.on_map = None
+        self.agent = None
+        self.weight = weight
+        self.bulk = bulk
+
+    def getInventoryThumbnail(self):
+        """Returns the inventory thumbnail of the object"""
+        # TODO: Implement properly after the objects database is in place
+        if self.image == None:
+            return "gui/inv_images/inv_litem.png"
+        else:
+            return self.image
+    
+class Container (DynamicObject, Scriptable):
+    """Gives objects the capability to hold other objects"""
+    class TooBig(Exception):
+        """Exception to be raised when the object is too big
+        to fit into container"""
+        pass
+    
+    class SlotBusy(Exception):
+        """Exception to be raised when the requested slot is occupied"""
+        pass
+    
+    class ItemSelf(Exception):
+        """Exception to be raised when trying to add the container as an item"""
+        pass
+  
+    def __init__ (self, capacity = 0, items = None, **kwargs):
+        DynamicObject.__init__(self, **kwargs)
+        Scriptable.__init__(self, **kwargs)
+        self.attributes.append("container")
+        self.items = {}
+        self.capacity = capacity
+        if items:
+            for item in items:
+                self.placeItem(item)
+        
+    def placeItem (self, item, index=None):
+        """Adds the provided carryable item to the inventory. 
+           Runs an 'onStoreItem' script, if present""" 
+        if item is self:
+            raise self.ItemSelf("Paradox: Can't contain myself")    
+        if not item.trueAttr ('carryable'):
+            raise TypeError ('%s is not carryable!' % item)
+        if self.capacity and self.getContentsBulk()+item.bulk > self.capacity:
+            raise self.TooBig ('%s is too big to fit into %s' % (item, self))
+        item.in_container = self
+        if index == None:
+            self._placeAtVacant(item)
+        else:
+            if index in self.items :
+                raise self.SlotBusy('Slot %d is busy in %s' % (index, 
+                                                               self.name))
+            self.items[index] = item
+
+        # Run any parpg associated with storing an item in the container
+        try:
+            if self.trueAttr ('scriptable'):
+                self.runScript('onPlaceItem')
+        except AttributeError :
+            pass
+
+    def _placeAtVacant(self, item):
+        """Places an item at a vacant slot"""
+        vacant = None
+        for i in range(len(self.items)):
+            if i not in self.items :
+                vacant = i
+        if vacant == None :
+            vacant = len(self.items)
+        self.items[vacant] = item
+    
+    def takeItem (self, item):
+        """Takes the listed item out of the inventory. 
+           Runs an 'onTakeItem' script"""        
+        if not item in self.items.values():
+            raise ValueError ('I do not contain this item: %s' % item)
+        del self.items[self.items.keys()[self.items.values().index(item)]]
+
+        # Run any parpg associated with popping an item out of the container
+        try:
+            if self.trueAttr ('scriptable'):
+                self.runScript('onTakeItem')
+        except AttributeError :
+            pass
+    
+    def replaceItem(self, old_item, new_item):
+        """Replaces the old item with the new one
+        @param old_item: Old item which is removed
+        @type old_item: Carryable
+        @param new_item: New item which is added
+        @type new_item: Carryable
+        """
+        old_index = self.indexOf(old_item.ID)
+        self.removeItem(old_item)
+        self.placeItem(new_item, old_index)
+        
+    def removeItem(self, item):
+        """Removes an item from the container, basically the same as 'takeItem'
+        but does run a different script. This should be used when an item is
+        destroyed rather than moved out.
+        Runs 'onRemoveItem' script
+        """
+        if not item in self.items.values():
+            raise ValueError ('I do not contain this item: %s' % item)
+        del self.items[self.items.keys()[self.items.values().index(item)]]
+
+        # Run any parpg associated with popping an item out of the container
+        try:
+            if self.trueAttr ('scriptable'):
+                self.runScript('onRemoveItem')
+        except AttributeError :
+            pass
+
+    def count (self, item_type = ""):
+        """Returns the number of items"""
+        if item_type:
+            ret_count = 0
+            for index in self.items :
+                if self.items[index].item_type == item_type:
+                    ret_count += 1
+            return ret_count
+        return len(self.items)   
+    
+    def getContentsBulk(self):
+        """Bulk of the container contents"""
+        return sum((item.bulk for item in self.items.values()))
+
+    def getItemAt(self, index):
+        return self.items[index]
+    
+    def indexOf(self, ID):
+        """Returns the index of the item with the passed ID"""
+        for index in self.items :
+            if self.items[index].ID == ID:
+                return index
+        return None
+
+    def findItemByID(self, ID):
+        """Returns the item with the passed ID"""
+        for i in self.items :
+            if self.items[i].ID == ID:
+                return self.items[i]
+        return None
+
+    def findItemByItemType(self, item_type):
+        """Returns the item with the passed item_type"""
+        for index in self.items :
+            if self.items[index].item_type == item_type:
+                return self.items[index]
+        return None
+
+    def findItem(self, **kwargs):
+        """Find an item in container by attributes. All params are optional.
+           @type name: String
+           @param name: If the name is non-unique, return first matching object
+           @type kind: String
+           @param kind: One of the possible object types
+           @return: The item matching criteria or None if none was found"""
+        for index in self.items :
+            if "name" in kwargs and self.items[index].name != kwargs["name"]:
+                continue
+            if "ID" in kwargs and self.items[index].ID != kwargs["ID"]:
+                continue
+            if "kind" in kwargs and not self.items[index].trueAttr(kwargs["kind"]):
+                continue
+            if "item_type" in kwargs and self.items[index].item_type != kwargs["item_type"]:
+                continue
+            return self.items[index]
+        return None    
+    
+    def serializeItems(self):
+        """Returns the items as a list"""
+        items = []
+        for index, item in self.items.iteritems():
+            item_dict = item.getStateForSaving()
+            item_dict["index"] = index
+            item_dict["type"] = item.item_type
+            items.append(item_dict)
+        return items
+    
+    def getStateForSaving(self):
+        """Returns state for saving
+        """
+        ret_state = DynamicObject.getStateForSaving(self)
+        ret_state["Items"] = self.serializeItems()
+        return ret_state
+        
+class Living (BaseObject):
+    """Objects that 'live'"""
+    def __init__ (self, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("living")
+        self.lives = True
+
+    def die(self):
+        """Kills the object"""
+        self.lives = False   
+
+class CharStats (BaseObject):
+    """Provides the object with character statistics"""
+    def __init__ (self, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("charstats")
+        
+class Wearable (BaseObject):
+    """Objects than can be weared"""
+    def __init__ (self, slots, **kwargs):
+        """Allows the object to be worn somewhere on the body (e.g. pants)"""
+        BaseObject.__init__(self)
+        self.attributes.append("wearable")
+        if isinstance(slots, tuple) :
+            self.slots = slots
+        else :
+            self.slots = (slots,)
+    
+class Usable (BaseObject):
+    """Allows the object to be used in some way (e.g. a Zippo lighter 
+       to make a fire)"""
+    def __init__ (self, actions = None, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("usable")
+        self.actions = actions or {}   
+        
+class Weapon (BaseObject):
+    """Allows the object to be used as a weapon"""
+    def __init__ (self, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("weapon")
+        
+class Destructable (BaseObject):
+    """Allows the object to be destroyed"""
+    def __init__ (self, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("destructable")
+        
+class Trapable (BaseObject):
+    """Provides trap slots to the object"""
+    def __init__ (self, **kwargs):
+        BaseObject.__init__(self)
+        self.attributes.append("trapable")