Source code for CoolWorld.world

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""World module.

Implements:

    class Conventions -- essentially naming conventions to use in lib.

    class World2D -- World class for 2D games.

    class World3D -- World class for 3D games.

    class WorldItem -- base class for all world components.

    class WorldItemInterface -- mixin interface for all WorldItem class
    hook methods.
"""

"""
    The MIT License (MIT)

    Copyright (c) 2015 Raphaël SEBAN

    Permission is hereby granted, free of charge, to any person
    obtaining a copy of this software and associated documentation
    files (the "Software"), to deal in the Software without
    restriction, including without limitation the rights to use, copy,
    modify, merge, publish, distribute, sublicense, and/or sell copies
    of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be
    included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
    BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
    ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
    CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
"""

import threading
try:
    from . import geometry
except:
    import geometry
# end try


[docs]class Conventions: """Conventions for all lib components. Only defines a class name feature and a naming convention. """ def __init__ (self, name=None): """Class constructor. Parameter: name -- object's arbitrary name (default: class name). If omitted or None, 'name' is replaced by object's class name. Once initialized, an arbitrary name becomes read-only attribute and cannot be updated any more. In any case, object's name is formatted to fit with naming convention. """ self.__name = self.naming_convention(name or self.classname()) # end def
[docs] def classname (self, instance=None): """Return class name. Parameter: instance -- object instance (default: None). If omitted or None, 'instance' is replaced by 'self' instance. """ return (instance or self).__class__.__name__ # end def
@property
[docs] def name (self): """Read-only attribute. Returns object's arbitrary name. """ return self.__name # end def
[docs] def naming_convention (self, name): """Return name formatted along with naming convention. Parameter: name -- object's arbitrary name. By default, naming convention is to lowercase(name). """ # here, names are lowercased # for case-insensitive indexing convention return str(name).lower() # end def # end class
[docs]class World2D (Conventions): """World class for 2D games. The World class manages all synchronization processes between world items it may contain during its lifecycle. """ DEFAULT_AREA = (640, 480) def __init__ (self, **options): """Class constructor. Parameters: options -- keyword arguments. Supported keyword arguments: name -- object's arbitrary name (default: class name). area -- world's playground area (default: DEFAULT_AREA). """ super().__init__(name=options.get("name")) self.options = options self.area = options.get("area") self._running = False self.__items = dict() # end def @property def area (self): """World's playground area - read-write attribute. This can be a simple tuple (width, height) or a more complex WorldArea2D(width, height) object. This attribute always returns a geometry.WorldArea2D object instance (or whatever is defined in 'area_type' attribute in subclasses), no matter what has initialized it. Example:: from CoolWorld import game world = game.World2D() world.area = (800, 600) print(world.area, type(world.area)) # geometry.WorldArea2D """ return self.__area # end def @area.setter
[docs] def area (self, value): self.__area = self.area_type(value or self.DEFAULT_AREA) # end def
@property
[docs] def area_type (self): """Preferred area type - read-only attribute. Current implementation returns geometry.WorldArea2D class type. This can be redefined in subclasses to fit more specific needs. """ return geometry.WorldArea2D # end def
[docs] def end (self, *args, **kw): """Try to break world's game loop. Override or extend this method in subclasses whenever you need more precise behaviour on game end request. """ self._running = False # end def
[docs] def fps_to_delay (self, fps): """Convert FPS value to time delay. Parameter: fps -- numeric value expressed in frames per second (FPS) unit. Should never be less than 1. Current implementation converts FPS value to time delay in milliseconds. Override this method in subclasses whenever you need another time delay unit e.g. in seconds rather than milliseconds. """ # in milliseconds return 1000 // fps # end def
[docs] def game_loop (self, fps, sequence): """Game's generic main loop. Parameters: fps -- required number of frames per second (steps/second). sequence -- sorted sequence of world item names to manage. Feel free to override or extend this method to best meet your own needs. """ # hook method to reimplement in subclass _delay = self.fps_to_delay(fps) _dt = _delay/1000.0 while self._running: for _item_name in sequence: self.get(_item_name).do_threaded_step(dt=_dt) # end for self.sleep(_dt) # end while # end def
[docs] def get (self, item_name, strict=True): """Get world item by its arbitrary name. Parameters: item_name -- world item's arbitrary name. strict -- strict mode (default: True). Raises KeyError if strict and no registered name found, remains silent otherwise. Use this method to get a world item object instance through its arbitrary name e.g. _player = self.world.get("player"). """ item_name = self.naming_convention(item_name) if strict: return self.items[item_name] else: return self.items.get(item_name) # end if # end def
[docs] def get_items (self, exclude=None): """Return list of registered world items. Parameter: exclude -- world item(s) to exclude (default: None). This can be just a single object instance or a list of object instances. Please, notice returned list of world items is an unordered Python set() object. """ _items = set(self.items.values()) try: _items.difference_update(exclude) except: _items.discard(exclude) # end try return _items # end def
@property
[docs] def items (self): """World items dictionary - read-only attribute. Use this attribute with caution, as any external change may lead to unpredictable behaviour. Unless you really know what you are doing, refer to world.items only for read-only purposes. """ return self.__items # end def
[docs] def register (self, world_item, name=None): """Register world item into current world. Parameters: world_item -- world item object instance to register. name -- arbitrary name (default: None). Can be any arbitrary name, provided it is a unique one in all game' scope. If parameter 'name' is not given, the world item instance will be registered with its own 'world_item.name' attribute. Registering will raise KeyError if given 'name' is already registered in current world. A registered world item is internally tagged with an on-the-fly 'world_item.registered_as_name' attribute. This is done for easy tracking of arbitrary name registering. """ name = self.naming_convention(name or world_item.name) if name in self.items: raise KeyError( "Item name '{}' is already registered.".format(name) ) else: self.items[name] = world_item world_item.registered_as_name = name # end if # end def
[docs] def run (self, sequence=None, fps=30): """Manage world items for game's main loop. Parameters: sequence -- sequential list of world item names (default: None). Should be a list of all world item names to execute in a precise sequential order. If not given or None, the sequential order will be evaluated as the sorted list of all registered world item names. fps -- required number of frames per second (default: 30). A game frame is a synonym for game step. This method first initializes all world items along with 'sequence' sequential order, then enters self.game_loop(fps, sequence) and after then finalizes all world items, yet along with 'sequence' sequential order. """ sequence = tuple(sequence or sorted(self.items.keys())) for _item_name in sequence: self.get(_item_name).initialize() # end for self._running = True self.game_loop(fps, sequence) for _item_name in sequence: _item = self.get(_item_name) _item.finalize() _item.leave_world() # end for self.wait() # end def
[docs] def sleep (self, delay): """Suspend world activity for a time delay. Override this hook method in subclasses whenever you need more precise behaviour to synchronize world's pace (FPS). """ # hook method import time time.sleep(delay) # end def
[docs] def unregister (self, item): """Unregister a world item. Parameter: item -- can be a world item arbitrary name (string) or a world item object instance. When 'item' is merely an arbitrary name, silently removes world item unique occurrence of that name, if exists. When 'item' is an actual world item object instance, silently removes ALL possible occurrences of that world item (multiple registering of a same instance with multiple arbitrary names). """ if isinstance(item, str): self.items.pop(self.naming_convention(item), None) else: # remove ALL occurrences of item for _name, _item in self.items.copy().items(): if _item is item: self.items.pop(_name, None) # end if # end for # end if # end def
[docs] def wait (self): """Wait until all world items are unregistered. Will call self.wait_do_events() method in a loop while waiting for world items to leave. """ # wait until all world items are gone (unregistered) while self.items: self.wait_do_events() # end while # end def
[docs] def wait_do_events (self, *args, **kw): """Update events while waiting for world items to leave. This hook method does nothing in the current implementation. Feel free to override this in subclasses whenever you need a more precise behaviour to update events while waiting for world items to all leave the current world. """ pass # end def # end class
[docs]class World3D (World2D): """World class for 3D games. The World class manages all synchronization processes between world items it contains. """ DEFAULT_AREA = (1000, 1000, 1000) @property
[docs] def area_type (self): """Preferred area type - read-only attribute. Current implementation returns geometry.WorldArea3D class type. This can be redefined in subclasses to fit more specific needs. """ return geometry.WorldArea3D # end def # end class
[docs]class WorldItemInterface: """Mixin interface. Gathers all hook methods to be reimplemented in subclasses. """
[docs] def do_step (self, *args, **kw): """Do a game frame (step) - hook method. Called at each step of world game's main loop. Current implementation does nothing. Override this in subclasses whenever you need some action at each step of game loop. """ pass # end def
[docs] def finalize (self): """Finalize world item - hook method. Called after exiting world game's main loop. Current implementation does nothing. Override this in subclasses whenever you need some deletion or garbage collection at the end of the game (or at the end of world item's lifecycle). """ pass # end def
[docs] def initialize (self): """Initialize world item - hook method. Called just before entering world game's main loop. Current implementation does nothing. Override this in subclasses whenever you need some initializations at the beginning of the game (or at the beginning of world item's lifecycle). """ pass # end def # end class
[docs]class WorldItem (Conventions, WorldItemInterface): """Base class for all World components. This is THE class to be subclassed each time you need a specific world item e.g. Sprite, Player, Enemy, View, ImageManager, SoundManager, MovementManager, CollisionManager and so on. """ def __init__ (self, world, name=None, **options): """Class constructor. Parameters: world -- parent world containing world item. name -- world item's arbitrary name (default: None). If None or omitted, will be replaced by current class name. options -- keyword arguments. Supported keyword arguments: -- none at this stage -- Notice: options are saved into self.options member attribute. """ Conventions.__init__(self, name) self.thread = None self.options = options self.world = world self.world.register(self) # end def
[docs] def do_threaded_step (self, *args, **kw): """Execute only one step of game loop in a separate thread of control. Will raise RuntimeError if previous step takes too long to execute, which somehow implies world item's step is globally too slow to fit with game's current pace. """ if self.thread and self.thread.is_alive(): raise RuntimeError( "World item '{}' takes too long to execute one step." .format(self.classname()) ) else: self.thread = threading.Thread( target=self.do_step, args=args, kwargs=kw, daemon=True ) self.thread.start() # end if # end def
[docs] def enter_world (self, world=None): """Enter a new world. Parameter: world -- new parent world (default: None). If None or omitted, will be replaced by 'self.world' member attribute. Will raise KeyError on registering if world item has already been registered to the given world and never been unregistered before. """ world = world or self.world world.register(self) # end def
[docs] def leave_world (self, world=None): """Leave world. Parameter: world -- new parent world (default: None). If None or omitted, will be replaced by 'self.world' member attribute. Will silently unregister world item from given world. """ world = world or self.world world.unregister(self) # end def
[docs] def move_to_world (self, world): """Move to a new world. Parameter: world -- new parent world to enter after having left current world behind. This method simply leaves current world and enters new given world. """ self.leave_world(self.world) self.world = world self.enter_world(self.world) # end def # end class
[docs]def run_demo (): class Player (WorldItem): def initialize (self): self.counter = 0 # end def def do_step (self, *args, **kw): if self.counter < 10: self.counter += 1 print("counter:", self.counter) else: self.world.end() # end if # end def # end class print("starting demo.") _world = World2D() _player = Player(_world) _world.run(fps=5) print("demo ended.") # end def
if __name__ == "__main__": run_demo() # end if