TStand90/tcod_tutorial_v2

Part 5: Enemy turn.

Closed this issue · 2 comments

Enemy turns were handed with a state in v1, but I think states should only be used to handle different kinds of player input and that enemy turn should be a method to call after the player finishes an action.

Actions might end up removing an entity from the entities set while it's being iterated over. So a copy of the set needs to be used for iteration. for entity in list(self.game_map.entities): would work but for entity in self.game_map.entities - {self.player}: makes a copy and prevents the need to compare every entity to player.

def enemy_turn(self) -> None:
    for entity in self.game_map.entities - {self.player}:
        if not isinstance(entity, Actor):
            continue  # This entity can't perform actions.
        if entity.fighter.hp <= 0:
            continue  # Entity was killed during this loop.
        ...  # Take action.

isinstance is a fairly slow check here. The alternatives would be to keep a separate set of Actors or to give Entity an on_turn method overridden by Actor, but I don't think the performance would be bad enough to require those.

Part 5 doesn't get into actual Entity actions, but I'm glad you're thinking ahead to it. For part 5 specifically, I think this is enough:

    def handle_enemy_turns(self):
        for entity in self.game_map.entities - {self.player}:
            print(f'The {entity.name} wonders when it will get to take a real turn.')

When we get to Part 6 (where the enemies to take real turns), what if we have a "can_act" property, which does all the necessary checks? It can check if the entity is an Actor and if it isn't dead.

Just assume I'm talking about part X and later in the issues posts. Ignore the code for future parts until they become relevant.

The thing about a property is it can't hint at the type of the class by itself. Which is why tcod's event.type had to be replaced with isinstance. At the very least you'd need to add assert isinstance or some other type cast in the property or after the check.

There's an alternative method which can be used. Which is to add an empty on_turn method to Entity and override it for Actor, the overridden method on Actor will know that the type for self is Actor.

class Entity:
    def on_turn(self, engine: Engine) -> None:
        """Called on each non-player entity during handle_enemy_turns."""


class Actor(Entity):
    def on_turn(self, engine: Engine) -> None:
        """Handle actions performed by an enemy actor."""
        if self.fighter.hp <= 0:
            continue
        print(f'The {self.name} wonders when it will get to take a real turn.')

...
def handle_enemy_turns(self):
    for entity in self.game_map.entities - {self.player}:
        entity.on_turn(self)

I don't know about the performance of this. I suspect it would be a little faster than isinstance.