fredericks does the roguelikedev tutorial - 2
This week I wanted to start by turning my “simple” entities into proper objects. Previously anything that moved was simply a row/column position dictionary. Nice for prototyping, bad for anything else.
Somewhat following a mishmash of multiple tutorials I created a base Entity
class and a child MoveableEntity
class, where Entity
is intended to not move (e.g., items). Every object also now gets a unique identifier so that I can track them, care of the uuid
Python class.
I may end up needing to refactor as I’m not truly following the tutorial and just using it as an excuse to work on yet another roguelike, however here is where we’re at:
# Base entity class
class Entity:
def __init__(self, _type, pos, entity_id=None, count=None):
assert _type in ENTITY_NAMES, "Error: ${0} not found in lookup table.".format(_type)
self.entity_id = entity_id
self._type = _type
self.pos = pos
self.count = count
def getTransmissable(self):
return {
'type': self._type,
'pos': self.pos,
'count': self.count,
}
# Entity that can move around the screen
class MoveableEntity(Entity):
def __init__(self, _type, pos, entity_id=None):
super().__init__(_type, pos, entity_id)
# particulars
self._type = _type
self.pos = pos
self.entity_id = entity_id # only used for logged in players
# stats
self.hp = LOOKUP_STATS['maxHP'][_type]
self.maxHP = LOOKUP_STATS['maxHP'][_type]
self.active = True
self.inventory = {}
def getTransmissable(self):
return {
'type': self._type,
'pos': self.pos,
'hp': self.hp,
'maxHP': self.maxHP,
'active': self.active,
'inventory': self.inventory,
}
Couple of points here that were important to me. I have a LOOKUP_STATS
dictionary in global space that serves as my overall balancing table for entity stats - who has what HP, etc. I also have an ENTITY_NAMES
list that is the global array of the names for all things that can be in the game (mainly as a check to make sure I didn’t typo anywhere when instantiating entities). Most likely I’ll move those to a dictionary that serves as a name/description table, but that’s for future me (insert technical debt here).
The other bit is the getTransmissable
function. This serves to break the object down into what gets sent along the socket so that we’re not packaging up the entire thing - only what matters to the frontend. There are probably better ways of doing this, but I’m trying to keep it as light as possible.
So now, I can instantiate everything as an Entity
or MoveableEntity
, neat.
Next up is to add in items to be scattered around for the player to pick up. Here I used my new objects and created a new list focused around items - in this case apples. Right now they serve no purpose other than to be picked up and added to the player’s inventory, however it works quite nicely.
With some additional socket work (i.e., also sending the items array as well as the player inventories) this behavior also works nicely with multiple players. Not too hard as the logic is pretty much the same as enemy updates and was mostly copy and paste.
This leads me to one future point that I should address. Right now all of my things in the game are separate in that I have a dictionary of players (keyed on the session ID provided by Flask), a list of enemies, and a list of items. It works, however it really isn’t to my liking for efficiency. At some point in the near future I’ll merge this into a single Entities object (or perhaps delineate Entities and MoveableEntities) and try to capture the logic into a bastardized Entity-Component pattern.
Last update for the week - laying the groundwork for multiple levels/floors/instances/etc. A new attribute was added to the position of all Entity
objects: level
.
I have a helper function that places the Entity
on a clear spot within the map (no walls, etc.):
def getRandomPos(self, level=1):
r = random.randint(0,self.NUM_ROWS-1)
c = random.randint(0,self.NUM_COLS-1)
while not self.isWalkable(c, r):
r = random.randint(0,self.NUM_ROWS-1)
c = random.randint(0,self.NUM_COLS-1)
return {'r': r, 'c': c, 'level': level}
Now, it takes in the desired level
of the object and includes it as something very easy to lookup. The change cascades all over the current code, however any time there is a lookup for row/column, now the level is checked as well. For instance, enemies only follow players on their level, socket data only sends current floor information relevant to the player, etc.
You may have noticed there aren’t any enemies in the last gif - that’s because I spawned them all on level 2 to check that my inventory/pickup system was working without needing to despawn them all:
def addEnemy(self):
return MoveableEntity("snek", self.getRandomPos(level=2), str(uuid.uuid4()))
That’s it for now - next week I’m hoping to add a camera system and/or a proper procedural content generation algorithm for the dungeon. Or focus on a user interface so I don’t need to keep calling console.log
any time I want to see data 🙄.