diff --git a/erynrl/__main__.py b/erynrl/__main__.py index a396d7a..cee3559 100644 --- a/erynrl/__main__.py +++ b/erynrl/__main__.py @@ -1,21 +1,16 @@ # Eryn Wells import argparse -import json -import logging -import logging.config import os.path import sys import tcod +from . import log from .engine import Configuration, Engine from .events import EventHandler from .geometry import Size -LOG = logging.getLogger('main') - CONSOLE_WIDTH, CONSOLE_HEIGHT = 80, 50 MAP_WIDTH, MAP_HEIGHT = 80, 45 - FONT = 'terminal16x16_gs_ro.png' def parse_args(argv, *a, **kw): @@ -24,23 +19,6 @@ def parse_args(argv, *a, **kw): args = parser.parse_args(argv) return args -def init_logging(args): - '''Set up the logging system by (preferrably) reading a logging configuration file.''' - logging_config_path = find_logging_config() - if logging_config_path: - with open(logging_config_path, encoding='utf-8') as logging_config_file: - logging_config = json.load(logging_config_file) - logging.config.dictConfig(logging_config) - LOG.info('Found logging configuration at %s', logging_config_path) - else: - root_logger = logging.getLogger('') - root_logger.setLevel(logging.DEBUG if args.debug else logging.INFO) - - stderr_handler = logging.StreamHandler() - stderr_handler.setFormatter(logging.Formatter("%(asctime)s %(name)s: %(message)s")) - - root_logger.addHandler(stderr_handler) - def walk_up_directories_of_path(path): while path and path != '/': path = os.path.dirname(path) @@ -51,38 +29,26 @@ def find_fonts_directory(): for parent_dir in walk_up_directories_of_path(__file__): possible_fonts_dir = os.path.join(parent_dir, 'fonts') if os.path.isdir(possible_fonts_dir): - LOG.info('Found fonts dir %s', possible_fonts_dir) + log.ROOT.info('Found fonts dir %s', possible_fonts_dir) break else: return None return possible_fonts_dir -def find_logging_config(): - '''Walk up the filesystem from this script to find a logging_config.json''' - for parent_dir in walk_up_directories_of_path(__file__): - possible_logging_config_file = os.path.join(parent_dir, 'logging_config.json') - if os.path.isfile(possible_logging_config_file): - LOG.info('Found logging config file %s', possible_logging_config_file) - break - else: - return None - - return possible_logging_config_file - def main(argv): args = parse_args(argv[1:], prog=argv[0]) - init_logging(args) + log.init() fonts_directory = find_fonts_directory() if not fonts_directory: - LOG.error("Couldn't find a fonts/ directory") + log.ROOT.error("Couldn't find a fonts/ directory") return -1 font = os.path.join(fonts_directory, FONT) if not os.path.isfile(font): - LOG.error("Font file %s doesn't exist", font) + log.ROOT.error("Font file %s doesn't exist", font) return -1 tileset = tcod.tileset.load_tilesheet(font, 16, 16, tcod.tileset.CHARMAP_CP437) diff --git a/erynrl/actions.py b/erynrl/actions.py index 01fffaf..ca3f447 100644 --- a/erynrl/actions.py +++ b/erynrl/actions.py @@ -18,18 +18,16 @@ Action : Base class of all actions WaitAction ''' -import logging from typing import Optional, TYPE_CHECKING from . import items +from . import log from .geometry import Direction from .object import Actor, Item if TYPE_CHECKING: from .engine import Engine -LOG = logging.getLogger('actions') - class ActionResult: '''The result of an Action. @@ -158,12 +156,12 @@ class BumpAction(MoveAction): else: entity_occupying_position = None - LOG.debug('Bumping %s into %s (in_bounds:%s walkable:%s overlaps:%s)', - self.actor, - new_position, - position_is_in_bounds, - position_is_walkable, - entity_occupying_position) + log.ACTIONS.debug('Bumping %s into %s (in_bounds:%s walkable:%s overlaps:%s)', + self.actor, + new_position, + position_is_in_bounds, + position_is_walkable, + entity_occupying_position) if not position_is_in_bounds or not position_is_walkable: return self.failure() @@ -180,7 +178,7 @@ class WalkAction(MoveAction): def perform(self, engine: 'Engine') -> ActionResult: new_position = self.actor.position + self.direction - LOG.debug('Moving %s to %s', self.actor, new_position) + log.ACTIONS.debug('Moving %s to %s', self.actor, new_position) self.actor.position = new_position return self.success() @@ -201,13 +199,13 @@ class MeleeAction(MoveAction): damage = self.actor.fighter.attack_power - self.target.fighter.defense if damage > 0 and self.target: - LOG.debug('%s attacks %s for %d damage!', self.actor, self.target, damage) + log.ACTIONS.debug('%s attacks %s for %d damage!', self.actor, self.target, damage) self.target.fighter.hit_points -= damage else: - LOG.debug('%s attacks %s but does no damage!', self.actor, self.target) + log.ACTIONS.debug('%s attacks %s but does no damage!', self.actor, self.target) if self.target.fighter.is_dead: - LOG.info('%s is dead!', self.target) + log.ACTIONS.info('%s is dead!', self.target) return ActionResult(self, alternate=DieAction(self.target)) return self.success() @@ -216,18 +214,18 @@ class WaitAction(Action): '''Wait a turn''' def perform(self, engine: 'Engine') -> ActionResult: - LOG.debug('%s is waiting a turn', self.actor) + log.ACTIONS.debug('%s is waiting a turn', self.actor) return self.success() class DieAction(Action): '''Kill an Actor''' def perform(self, engine: 'Engine') -> ActionResult: - LOG.debug('%s dies', self.actor) + log.ACTIONS.debug('%s dies', self.actor) engine.entities.remove(self.actor) if self.actor.yields_corpse_on_death: - LOG.debug('%s leaves a corpse behind', self.actor) + log.ACTIONS.debug('%s leaves a corpse behind', self.actor) corpse = Item(kind=items.Corpse, name=f'{self.actor.name} Corpse', position=self.actor.position) return ActionResult(self, alternate=DropItemAction(self.actor, corpse)) diff --git a/erynrl/ai.py b/erynrl/ai.py index 75ca3d3..3fc1904 100644 --- a/erynrl/ai.py +++ b/erynrl/ai.py @@ -1,12 +1,12 @@ # Eryn Wells -import logging import random from typing import TYPE_CHECKING, List, Optional import numpy as np import tcod +from . import log from .actions import Action, BumpAction, WaitAction from .components import Component from .geometry import Direction, Point @@ -15,8 +15,6 @@ from .object import Entity if TYPE_CHECKING: from .engine import Engine -LOG = logging.getLogger('ai') - class AI(Component): def __init__(self, entity: Entity) -> None: super().__init__() @@ -34,7 +32,7 @@ class HostileEnemy(AI): radius=self.entity.sight_radius) if engine.map.visible[tuple(self.entity.position)]: - LOG.debug("AI for %s", self.entity) + log.AI.debug("AI for %s", self.entity) hero_position = engine.hero.position hero_is_visible = visible_tiles[hero_position.x, hero_position.y] @@ -46,13 +44,13 @@ class HostileEnemy(AI): entity_position = self.entity.position if engine.map.visible[tuple(self.entity.position)]: - LOG.debug('|-> Path to hero %s', path_to_hero) + log.AI.debug('|-> Path to hero %s', path_to_hero) next_position = path_to_hero.pop(0) if len(path_to_hero) > 1 else hero_position direction_to_next_position = entity_position.direction_to_adjacent_point(next_position) if engine.map.visible[tuple(self.entity.position)]: - LOG.info('`-> Hero is visible to %s, bumping %s (%s)', self.entity, direction_to_next_position, next_position) + log.AI.info('`-> Hero is visible to %s, bumping %s (%s)', self.entity, direction_to_next_position, next_position) return BumpAction(self.entity, direction_to_next_position) else: @@ -68,13 +66,13 @@ class HostileEnemy(AI): tile_is_walkable = engine.map.tile_is_walkable(new_position) if not overlaps_existing_entity and tile_is_walkable: if engine.map.visible[tuple(self.entity.position)]: - LOG.info('Hero is NOT visible to %s, bumping %s randomly', self.entity, direction) + log.AI.info('Hero is NOT visible to %s, bumping %s randomly', self.entity, direction) action = BumpAction(self.entity, direction) break else: # If this entity somehow can't move anywhere, just wait if engine.map.visible[tuple(self.entity.position)]: - LOG.info("Hero is NOT visible to %s and it can't move anywhere, waiting", self.entity) + log.AI.info("Hero is NOT visible to %s and it can't move anywhere, waiting", self.entity) action = WaitAction(self.entity) return action diff --git a/erynrl/engine.py b/erynrl/engine.py index fbe2895..e168ae5 100644 --- a/erynrl/engine.py +++ b/erynrl/engine.py @@ -2,21 +2,19 @@ '''Defines the core game engine.''' -import logging import random from dataclasses import dataclass from typing import MutableSet import tcod +from . import log from . import monsters from .ai import HostileEnemy from .geometry import Size from .map import Map from .object import Entity, Hero, Monster -LOG = logging.getLogger('engine') - @dataclass class Configuration: map_size: Size @@ -66,7 +64,7 @@ class Engine: else: monster = Monster(monsters.Orc, ai_class=HostileEnemy, position=random_start_position) - LOG.info('Spawning %s', monster) + log.ENGINE.info('Spawning %s', monster) self.entities.add(monster) self.update_field_of_view() diff --git a/erynrl/events.py b/erynrl/events.py index 46295d2..d6d4d2a 100644 --- a/erynrl/events.py +++ b/erynrl/events.py @@ -2,11 +2,11 @@ '''Defines event handling mechanisms.''' -import logging from typing import Optional, TYPE_CHECKING import tcod +from . import log from .actions import Action, ActionResult, ExitAction, RegenerateRoomsAction, BumpAction, WaitAction from .geometry import Direction from .object import Actor @@ -14,9 +14,6 @@ from .object import Actor if TYPE_CHECKING: from .engine import Engine -LOG = logging.getLogger('events') -ACTIONS_TREE_LOG = logging.getLogger('actions.tree') - class EventHandler(tcod.event.EventDispatch[Action]): '''Handler of `tcod` events''' @@ -35,11 +32,11 @@ class EventHandler(tcod.event.EventDispatch[Action]): # Unhandled event. Ignore it. if not action: - LOG.debug('Unhandled event: %s', event) + log.EVENTS.debug('Unhandled event: %s', event) return - ACTIONS_TREE_LOG.info('Processing Hero Actions') - ACTIONS_TREE_LOG.info('|-> %s', action.actor) + log.ACTIONS_TREE.info('Processing Hero Actions') + log.ACTIONS_TREE.info('|-> %s', action.actor) result = self.perform_action_until_done(action) @@ -54,7 +51,7 @@ class EventHandler(tcod.event.EventDispatch[Action]): self.engine.entities, key=lambda e: e.position.euclidean_distance_to(hero_position)) - ACTIONS_TREE_LOG.info('Processing Entity Actions') + log.ACTIONS_TREE.info('Processing Entity Actions') for i, ent in enumerate(entities): if not isinstance(ent, Actor): @@ -65,7 +62,7 @@ class EventHandler(tcod.event.EventDispatch[Action]): continue if self.engine.map.visible[tuple(ent.position)]: - ACTIONS_TREE_LOG.info('%s-> %s', '|' if i < len(entities) - 1 else '`', ent) + log.ACTIONS_TREE.info('%s-> %s', '|' if i < len(entities) - 1 else '`', ent) action = ent_ai.act(self.engine) self.perform_action_until_done(action) @@ -76,17 +73,17 @@ class EventHandler(tcod.event.EventDispatch[Action]): '''Perform the given action and any alternate follow-up actions until the action chain is done.''' result = action.perform(self.engine) - if ACTIONS_TREE_LOG.isEnabledFor(logging.INFO) and self.engine.map.visible[tuple(action.actor.position)]: + if log.ACTIONS_TREE.isEnabledFor(log.INFO) and self.engine.map.visible[tuple(action.actor.position)]: if result.alternate: alternate_string = f'{result.alternate.__class__.__name__}[{result.alternate.actor.symbol}]' else: alternate_string = str(result.alternate) - ACTIONS_TREE_LOG.info('| %s-> %s => success=%s done=%s alternate=%s', - '|' if not result.success or not result.done else '`', - action, - result.success, - result.done, - alternate_string) + log.ACTIONS_TREE.info('| %s-> %s => success=%s done=%s alternate=%s', + '|' if not result.success or not result.done else '`', + action, + result.success, + result.done, + alternate_string) while not result.done: action = result.alternate @@ -94,12 +91,12 @@ class EventHandler(tcod.event.EventDispatch[Action]): result = action.perform(self.engine) - if ACTIONS_TREE_LOG.isEnabledFor(logging.INFO) and self.engine.map.visible[tuple(action.actor.position)]: + if log.ACTIONS_TREE.isEnabledFor(log.INFO) and self.engine.map.visible[tuple(action.actor.position)]: if result.alternate: alternate_string = f'{result.alternate.__class__.__name__}[{result.alternate.actor.symbol}]' else: alternate_string = str(result.alternate) - ACTIONS_TREE_LOG.info('| %s-> %s => success=%s done=%s alternate=%s', + log.ACTIONS_TREE.info('| %s-> %s => success=%s done=%s alternate=%s', '|' if not result.success or not result.done else '`', action, result.success, diff --git a/erynrl/log.py b/erynrl/log.py new file mode 100644 index 0000000..d61295e --- /dev/null +++ b/erynrl/log.py @@ -0,0 +1,69 @@ +# Eryn Wells + +''' +Initializes and sets up +''' + +import json +import logging +import logging.config +import os.path + +# These are re-imports so clients of this module don't have to also import logging +# pylint: disable=unused-import +from logging import CRITICAL, DEBUG, ERROR, FATAL, INFO, NOTSET, WARN, WARNING + +def _log_name(*components): + return '.'.join(['erynrl'] + list(components)) + +ROOT = logging.getLogger(_log_name()) +AI = logging.getLogger(_log_name('ai')) +ACTIONS = logging.getLogger(_log_name('actions')) +ACTIONS_TREE = logging.getLogger(_log_name('actions', 'tree')) +ENGINE = logging.getLogger(_log_name('engine')) +EVENTS = logging.getLogger(_log_name('events')) +MAP = logging.getLogger(_log_name('map')) + +def walk_up_directories_of_path(path): + '''Walk up a path, yielding each directory, until the root of the filesystem is found''' + while path and path != '/': + if os.path.isdir(path): + yield path + path = os.path.dirname(path) + +def find_logging_config(): + '''Walk up the filesystem from this script to find a logging_config.json''' + for parent_dir in walk_up_directories_of_path(__file__): + possible_logging_config_file = os.path.join(parent_dir, 'logging_config.json') + if os.path.isfile(possible_logging_config_file): + ROOT.info('Found logging config file %s', possible_logging_config_file) + break + else: + return None + + return possible_logging_config_file + +def init(config_file=None): + ''' + Set up the logging system by (preferrably) reading a logging configuration file. + + Parameters + ---------- + config_file : str + Path to a file containing a Python logging configuration in JSON + ''' + logging_config_path = config_file if config_file else find_logging_config() + + if os.path.isfile(logging_config_path): + with open(logging_config_path, encoding='utf-8') as logging_config_file: + logging_config = json.load(logging_config_file) + logging.config.dictConfig(logging_config) + ROOT.info('Found logging configuration at %s', logging_config_path) + else: + root_logger = logging.getLogger('') + root_logger.setLevel(logging.DEBUG) + + stderr_handler = logging.StreamHandler() + stderr_handler.setFormatter(logging.Formatter("%(asctime)s %(name)s: %(message)s")) + + root_logger.addHandler(stderr_handler) diff --git a/erynrl/map.py b/erynrl/map.py index 2a993d3..33b3bbd 100644 --- a/erynrl/map.py +++ b/erynrl/map.py @@ -1,6 +1,5 @@ # Eryn Wells -import logging import random from dataclasses import dataclass from typing import Iterator, List, Optional @@ -8,11 +7,10 @@ from typing import Iterator, List, Optional import numpy as np import tcod +from . import log from .geometry import Direction, Point, Rect, Size from .tile import Empty, Floor, Shroud, Wall -LOG = logging.getLogger('map') - class Map: def __init__(self, size: Size): self.size = size @@ -120,7 +118,7 @@ class RoomsAndCorridorsGenerator(MapGenerator): node_bounds = self.__rect_from_bsp_node(node) if node.children: - LOG.debug(node_bounds) + log.MAP.debug(node_bounds) left_room: RectangularRoom = getattr(node.children[0], room_attrname) right_room: RectangularRoom = getattr(node.children[1], room_attrname) @@ -128,8 +126,8 @@ class RoomsAndCorridorsGenerator(MapGenerator): left_room_bounds = left_room.bounds right_room_bounds = right_room.bounds - LOG.debug(' left: %s, %s', node.children[0], left_room_bounds) - LOG.debug('right: %s, %s', node.children[1], right_room_bounds) + log.MAP.debug(' left: %s, %s', node.children[0], left_room_bounds) + log.MAP.debug('right: %s, %s', node.children[1], right_room_bounds) start_point = left_room_bounds.midpoint end_point = right_room_bounds.midpoint @@ -140,16 +138,16 @@ class RoomsAndCorridorsGenerator(MapGenerator): else: corner = Point(start_point.x, end_point.y) - LOG.debug('Digging a tunnel between %s and %s with corner %s', start_point, end_point, corner) - LOG.debug('|-> start: %s', left_room_bounds) - LOG.debug('`-> end: %s', right_room_bounds) + log.MAP.debug('Digging a tunnel between %s and %s with corner %s', start_point, end_point, corner) + log.MAP.debug('|-> start: %s', left_room_bounds) + log.MAP.debug('`-> end: %s', right_room_bounds) for x, y in tcod.los.bresenham(tuple(start_point), tuple(corner)).tolist(): tiles[x, y] = Floor for x, y in tcod.los.bresenham(tuple(corner), tuple(end_point)).tolist(): tiles[x, y] = Floor else: - LOG.debug('%s (room) %s', node_bounds, node) + log.MAP.debug('%s (room) %s', node_bounds, node) # Generate a room size between minimum_room_size and maximum_room_size. The minimum value is # straight-forward, but the maximum value needs to be clamped between minimum_room_size and the size of @@ -162,7 +160,7 @@ class RoomsAndCorridorsGenerator(MapGenerator): node.y + self.rng.randint(1, max(1, node.height - size.height - 1))) bounds = Rect(origin, size) - LOG.debug('`-> %s', bounds) + log.MAP.debug('`-> %s', bounds) room = RectangularRoom(bounds) setattr(node, room_attrname, room) diff --git a/logging_config.json b/logging_config.json index a770168..d42487e 100644 --- a/logging_config.json +++ b/logging_config.json @@ -15,30 +15,30 @@ } }, "loggers": { - "ai": { + "erynrl.ai": { "level": "ERROR", "handlers": ["console"], "propagate": false }, - "actions": { + "erynrl.actions": { "level": "INFO", "handlers": ["console"] }, - "actions.movement": { + "erynrl.actions.movement": { "level": "ERROR", "handlers": ["console"] }, - "actions.tree": { + "erynrl.actions.tree": { "level": "INFO", "handlers": ["console"], "propagate": false }, - "events": { + "erynrl.events": { "level": "WARN", "handlers": ["console"], "propagate": false }, - "visible": { + "erynrl.visible": { "level": "DEBUG", "handlers": ["console"] }