Refactor MovePlayerAction into a few different Action subclasses. Move direction to a parent MoveAction, and create three new subclasses of MoveAction: - BumpAction: perform the test that an action can be performed in the given direction - WalkAction, take a step in the given direction - MeleeAction, attack another Entity in the given direction Add an ActionResult class that communicates the result of performing an Action. - ActionResult.succeeded indicates whether the action succeeded. - ActionResult.done indicates if the action is fully complete or requires followup. - ActionResult.alternate specifies the follow-up action to perform. Convert all the key handling actions to BumpActions. In the Engine's event handler method, loop until an action is completed, performing specified alternate actions until the result object indicates the action is done.
125 lines
4.2 KiB
Python
125 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
# Eryn Wells <eryn@erynwells.me>
|
|
|
|
'''Defines the core game engine.'''
|
|
|
|
import logging
|
|
import random
|
|
from dataclasses import dataclass
|
|
from typing import MutableSet
|
|
|
|
import tcod
|
|
|
|
from . import monsters
|
|
from .events import EventHandler
|
|
from .geometry import Direction, Size
|
|
from .map import Map
|
|
from .monsters import Monster
|
|
from .object import Entity, Hero
|
|
|
|
LOG = logging.getLogger('engine')
|
|
EVENT_LOG = logging.getLogger('events')
|
|
|
|
@dataclass
|
|
class Configuration:
|
|
map_size: Size
|
|
|
|
class Engine:
|
|
def __init__(self, event_handler: EventHandler, configuration: Configuration):
|
|
self.event_handler = event_handler
|
|
self.configuration = configuration
|
|
|
|
self.rng = tcod.random.Random()
|
|
self.map = Map(configuration.map_size)
|
|
|
|
first_room = self.map.generator.rooms[0]
|
|
hero_start_position = first_room.center
|
|
self.hero = Hero(position=hero_start_position)
|
|
|
|
self.entities: MutableSet[Entity] = {self.hero}
|
|
for room in self.map.rooms:
|
|
should_spawn_monster_chance = random.random()
|
|
if should_spawn_monster_chance < 0.4:
|
|
continue
|
|
|
|
floor = list(room.walkable_tiles)
|
|
while True:
|
|
random_start_position = random.choice(floor)
|
|
if not any(ent.position == random_start_position for ent in self.entities):
|
|
break
|
|
|
|
for _ in range(2):
|
|
spawn_monster_chance = random.random()
|
|
if spawn_monster_chance > 0.8:
|
|
monster = Monster(monsters.Troll, position=random_start_position)
|
|
else:
|
|
monster = Monster(monsters.Orc, position=random_start_position)
|
|
|
|
LOG.info('Spawning monster %s', monster)
|
|
self.entities.add(monster)
|
|
|
|
self.update_field_of_view()
|
|
|
|
def handle_event(self, event: tcod.event.Event):
|
|
action = self.event_handler.dispatch(event)
|
|
|
|
if not action:
|
|
return
|
|
|
|
result = action.perform(self, self.hero)
|
|
LOG.debug('Performed action success=%s done=%s alternate=%s', result.success, result.done, result.alternate)
|
|
|
|
while not result.done:
|
|
alternate = result.alternate
|
|
assert alternate is not None, f'Action {result.action} incomplete but no alternate action given'
|
|
|
|
result = alternate.perform(self, self.hero)
|
|
LOG.debug('Performed action success=%s done=%s alternate=%s', result.success, result.done, result.alternate)
|
|
|
|
if result.success:
|
|
LOG.info('Action succeded!')
|
|
break
|
|
|
|
if result.done:
|
|
LOG.info('Action failed!')
|
|
break
|
|
|
|
if not result.success and result.done:
|
|
return
|
|
|
|
directions = list(Direction.all())
|
|
moved_entities: MutableSet[Entity] = {self.hero}
|
|
|
|
for ent in self.entities:
|
|
if ent == self.hero:
|
|
continue
|
|
|
|
while True:
|
|
new_position = ent.position + random.choice(directions)
|
|
overlaps_with_previously_moved_entity = any(new_position == moved_ent.position for moved_ent in moved_entities)
|
|
if not overlaps_with_previously_moved_entity and self.map.tile_is_walkable(new_position):
|
|
ent.position = new_position
|
|
moved_entities.add(ent)
|
|
break
|
|
|
|
self.update_field_of_view()
|
|
|
|
def print_to_console(self, console):
|
|
'''Print the whole game to the given console.'''
|
|
self.map.print_to_console(console)
|
|
|
|
for ent in self.entities:
|
|
# Only print entities that are in the field of view
|
|
if not self.map.visible[tuple(ent.position)]:
|
|
continue
|
|
ent.print_to_console(console)
|
|
|
|
def update_field_of_view(self) -> None:
|
|
'''Compute visible area of the map based on the player's position and point of view.'''
|
|
self.map.visible[:] = tcod.map.compute_fov(
|
|
self.map.tiles['transparent'],
|
|
tuple(self.hero.position),
|
|
radius=8)
|
|
|
|
# Visible tiles should be added to the explored list
|
|
self.map.explored |= self.map.visible
|