pythonoopdesign-patternspygameroguelike

Use composition, strategy pattern and dictionary to better instantiate class stored in dictionary


I develop a RogueLike in python, and I try to make my best with OOP and my little knowledge to construct a python course for student.

mapRogue = ['~~~~~~~~~~',
            '~~~~.....Y',
            'YYYYY+YYYY',
            'YYYY....YY']

I want to transform this string map into 2D list containing object defining the nature of my tile in the RogueLike. For that I decide to use a dictionary to map character key and class to instantiate when I read this variable mapRogue.

I find a solution using inheritance, but imho this code is not really as elegant as i want, and probably not very flexible if I want to add other type of tile behavior later.

DOOR class using inheritance

class Tile(object):
    #a tile of the map and its properties
    def __init__(self, name, char, position, blocked, color=(255, 255, 255), bgcolor=(0, 0, 0)):
        self.name = name
        self.blocked = blocked
        self.char = char
        self.color = color
        self.bgcolor = bgcolor
        self.position = position

class Door(Tile):
    def __init__(self,  name, char, position, blocked, bgcolor, key,color=(255, 255, 255), open=False ):
        Tile.__init__( self,name, char, position, blocked, color, bgcolor)
        self.state = open
        self.key = key

    def opening(self, key):
        if self.key == key:
            self.state = True

tilesObject = {".": {"name": 'floor', "obj": Tile, "bgcolor": (233, 207, 177), "block": False},
               "Y": {"name": 'forest', "obj": Tile, "bgcolor": (25, 150, 64), "block": True},
               "~": {"name": 'water', "obj": Tile, "bgcolor": (10, 21, 35), "block": False},
               "+": {"name": 'doors', "obj": Door, "bgcolor": (10, 10, 25), "block": False}}
import types
def load(mymap):
    tileMap = []
    x, y = (0,0)
    for line in mymap:
        tileLine = []
        for value in line:
            try:
                tile = tilesObject[value]
            except KeyError:
                return "Error on key"
            if tile["obj"].__name__ ==  "Door":
                obj = tile["obj"](name=tile["name"], position=(x, y), char=value, blocked=tile["block"],bgcolor=tile["bgcolor"], key="42isTheKey", open=False)
            else:
                obj = tile["obj"](name=tile["name"], position=(x, y), char=value, blocked=tile["block"],bgcolor=tile["bgcolor"])

            x += 1
            tileLine.append(obj)
        x = 0
        y += 1
        tileMap.append(tileLine)
    return tileMap


for line in load(mapRogue):
    for obj in line:
        print obj , "\n"

DOOR class using composition

I suspect there is an other answer using composition and/or strategy pattern, so i try to decorate the Tile object with Door behavior, but i'm blocked with this dictionnary ...

Actually i try multiple solution without success, do you have a proposition to help me to solve this problem of conception using elegant oop and python ?

class Tile(object):
    #a tile of the map and its properties
    def __init__(self, name, char, position, blocked, color=(255, 255, 255), bgcolor=(0, 0, 0), door=None):
        self.name = name
        self.blocked = blocked
        self.char = char
        self.color = color
        self.bgcolor = bgcolor
        self.door = door
        self.position = position

# Door decorate the Tile object using composition
class Door(object):
    def __init__(self, key, open=False):
        self.state = open
        self.key = key

    def opening(self, key):
        if self.key == key:
            self.state = True

tilesObject = {".": {"name": 'floor', "obj": Tile, "bgcolor": (233, 207, 177), "block": False},
               "Y": {"name": 'forest', "obj": Tile, "bgcolor": (25, 150, 64), "block": True},
               "~": {"name": 'water', "obj": Tile, "bgcolor": (10, 21, 35), "block": False},
               "+": {"name": 'doors', "obj": Door, "bgcolor": (10, 10, 25), "block": False}}

def load(mymap):
    tileMap = []
    x, y = (0,0)
    for line in mymap:
        tileLine = []
        for value in line:
            try:
                tile = tilesObject[value]
            except KeyError:
                return "Error on key"

            # Here i need to detect when obj is Door 
                    # because i need to define a special Tile 
                    # decorated by Door behavior, 
                    # so it seems this is not a good solution :/

            x += 1
            tileLine.append(obj)
        x = 0
        y += 1
        tileMap.append(tileLine)
    return tileMap

An update with some informations:

Thanks for answer @User and @Hyperborreus, you're right, I simplify my example here, and in my code, I have two layers:

Using pygame, I display all my Tiles object using a draw_tile() function.

So at this point I need a link between Door and Tile class to compute correctly a fov for player later, because Door have behavior and limit the vision of my character (with an attributes blocked or fovState). After that, I drawn all gameObject, on top of these already drawed Tile surfaces. Door is part of computation only specific to Tile, and other things in roguelike so that explain why I define the Door like that I hope.

So probably you're right with your proposition of game definition dictionary, I need to change the way i instantiate object, the oop definition of Door / Tiles rest the same, but when I read the initial string map which contain item, door and also static object, I separate gameObject instantiation and Tile instantiation..

The idea of dictionary to instantiate element on a rogueLike map defined in string list is based on the idea founded here: https://bitbucket.org/BigYellowCactus/dropout/

Perhaps the creator of this code, @dominic-kexel can also help us to on this point?


Solution

  • IMHO, you should make a distinction between "tiles" (the underlying basemap) and "objects" (things the player can interact with, like doors that open, dragons that attack, or pits that kill).

    If you want to compare this with a 3D-video game, the "tiles" would be the environment you cannot interact with, and the "objects" would be the clickable things.

    The mere tiles can be instances of one single class and hold the information relevant and common to all tiles, like rendering hints (which character in which colour) or movement aspects (can be passed, movement speed, etc).

    The objects are then placed on top of the tiles.

    Imagine you have a map with a lot of floor and walls, and at two positions you have two doors. All "tiles" behave the same (you can walk on floor, no matter which floor tile) but you will butt your head against a wall (no matter where the wall is). But the doors are different: One door requires the "Green Key" and the other door requires the "Embroidered Key of the Dissident Pixie".

    This difference is where your if-issue arises. The door needs extra information. In order to define a map, you need all tiles (identical within each class) and another lists of objects placed on certain tiles (each object different).

    Doors, Wells, Dragons, Switches etc could inherit from a common base class which implements standard actions like "inspect", "interact", "attack", "yell at", and maybe special interfaces for special actions.

    So a complete game definition could look like this:

    game = {'baseMap': '#here comes your 2D array of tiles',
    'objects': [ {'class': Door, 'position': (x, y), 'other Door arguments': ...}, 
    {'class': Door, 'position': (x2, y2), 'other Door arguments': ...},
    {'class': Dragon, 'position': (x3, y3), 'dragon arguments': ...}, ] }
    

    Then for instantiating the actual objects (object in the OO sense, not in the game sense), walk throught this definition, and call the c'tor of each object with the dictionary items as keyword-arguments (double-asterisk). This is only one possible approach of many.

    For rendering, display the tile presentation if the tile is empty, or the object representation if there is an object on the tile.


    This is what I mean with double-asterisk:

    class Door:
        def __init__ (self, position, colour, creaking = True):
            print (position, colour, creaking)
    
    objDefs = [...,
              {'class': Door, 'kwargs': {'position': (2, 3), 'colour': 'black'} },
              ...]
    
    #Here you actually iterate over objDefs
    objDef = objDefs [1]
    obj = objDef ['class'] (**objDef ['kwargs'] )
    

    Big Edit:

    This is an idea of how one could implement the rendering of the map with both tiles and objects. (Just my two cents):

    #! /usr/bin/python3.2
    
    colours = {'white': 7, 'green': 2, 'blue': 4, 'black': 0, 'yellow': 3}
    
    class Tile:
        data = {'.': ('Floor', 'white', True),
            'Y': ('Forest', 'green', False),
            '~': ('Water', 'blue', False) }
    
        def __init__ (self, code, position):
            self.code = code
            self.position = position
            self.name, self.colour, self.passable = Tile.data [code]
    
        def __str__ (self):
            return '\x1b[{}m{}'.format (30 + colours [self.colour], self.code)
    
    class GameObject:
        #here got he general interfaces common to all game objects
        def __str__ (self):
            return '\x1b[{}m{}'.format (30 + colours [self.colour], self.code)
    
    class Door (GameObject):
        def __init__ (self, code, position, colour, key):
            self.code = code
            self.position = position
            self.colour = colour
            self.key = key
    
        def close (self): pass
            #door specific interface
    
    class Dragon (GameObject):
        def __init__ (self, code, position, colour, stats):
            self.code = code
            self.position = position
            self.colour = colour
            self.stats = stats
    
        def bugger (self): pass
            #dragon specific interface
    
    class Map:
        def __init__ (self, codeMap, objects):
            self.tiles = [ [Tile (c, (x, y) ) for x, c in enumerate (line) ] for y, line in enumerate (codeMap) ]
            self.objects = {obj ['args'] ['position']: obj ['cls'] (**obj ['args'] ) for obj in objects}
    
        def __str__ (self):
            return '\n'.join (
                ''.join (str (self.objects [ (x, y) ] if (x, y) in self.objects else tile)
                    for x, tile in enumerate (line) )
                for y, line in enumerate (self.tiles)
                ) + '\n\x1b[0m'
    
    mapRouge = ['~~~~~~~~~~',
                '~~~~.....Y',
                'YYYYY.YYYY',
                'YYYY....YY']
    
    objects = [ {'cls': Door,
            'args': {'code': '.', 'position': (5, 2), 'colour': 'black',
            'key': 'Ancient Key of Constipation'} },
        {'cls': Dragon,
            'args': {'code': '@',  'position': (7, 3), 'colour': 'yellow',
            'stats': {'ATK': 20, 'DEF': 20} } } ]
    
    theMap = Map (mapRouge, objects)
    print (theMap)
    

    And this is the result:

    enter image description here