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.
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"
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
Thanks for answer @User and @Hyperborreus, you're right, I simplify my example here, and in my code, I have two layers:
Tile
which don't move, GameObjects
which can move, attack, defend, and a lot of other function decorated using composition
like in this tutorial here 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?
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: