#!/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