Move all the logging to log.py and prefix all the log names with "erynrl"

This commit is contained in:
Eryn Wells 2022-05-12 20:40:06 -07:00
parent 5d4e0cff3d
commit ce63c825b0
8 changed files with 126 additions and 102 deletions

View file

@ -1,21 +1,16 @@
# Eryn Wells <eryn@erynwells.me> # Eryn Wells <eryn@erynwells.me>
import argparse import argparse
import json
import logging
import logging.config
import os.path import os.path
import sys import sys
import tcod import tcod
from . import log
from .engine import Configuration, Engine from .engine import Configuration, Engine
from .events import EventHandler from .events import EventHandler
from .geometry import Size from .geometry import Size
LOG = logging.getLogger('main')
CONSOLE_WIDTH, CONSOLE_HEIGHT = 80, 50 CONSOLE_WIDTH, CONSOLE_HEIGHT = 80, 50
MAP_WIDTH, MAP_HEIGHT = 80, 45 MAP_WIDTH, MAP_HEIGHT = 80, 45
FONT = 'terminal16x16_gs_ro.png' FONT = 'terminal16x16_gs_ro.png'
def parse_args(argv, *a, **kw): def parse_args(argv, *a, **kw):
@ -24,23 +19,6 @@ def parse_args(argv, *a, **kw):
args = parser.parse_args(argv) args = parser.parse_args(argv)
return args 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): def walk_up_directories_of_path(path):
while path and path != '/': while path and path != '/':
path = os.path.dirname(path) path = os.path.dirname(path)
@ -51,38 +29,26 @@ def find_fonts_directory():
for parent_dir in walk_up_directories_of_path(__file__): for parent_dir in walk_up_directories_of_path(__file__):
possible_fonts_dir = os.path.join(parent_dir, 'fonts') possible_fonts_dir = os.path.join(parent_dir, 'fonts')
if os.path.isdir(possible_fonts_dir): 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 break
else: else:
return None return None
return possible_fonts_dir 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): def main(argv):
args = parse_args(argv[1:], prog=argv[0]) args = parse_args(argv[1:], prog=argv[0])
init_logging(args) log.init()
fonts_directory = find_fonts_directory() fonts_directory = find_fonts_directory()
if not 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 return -1
font = os.path.join(fonts_directory, FONT) font = os.path.join(fonts_directory, FONT)
if not os.path.isfile(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 return -1
tileset = tcod.tileset.load_tilesheet(font, 16, 16, tcod.tileset.CHARMAP_CP437) tileset = tcod.tileset.load_tilesheet(font, 16, 16, tcod.tileset.CHARMAP_CP437)

View file

@ -18,18 +18,16 @@ Action : Base class of all actions
WaitAction WaitAction
''' '''
import logging
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from . import items from . import items
from . import log
from .geometry import Direction from .geometry import Direction
from .object import Actor, Item from .object import Actor, Item
if TYPE_CHECKING: if TYPE_CHECKING:
from .engine import Engine from .engine import Engine
LOG = logging.getLogger('actions')
class ActionResult: class ActionResult:
'''The result of an Action. '''The result of an Action.
@ -158,12 +156,12 @@ class BumpAction(MoveAction):
else: else:
entity_occupying_position = None entity_occupying_position = None
LOG.debug('Bumping %s into %s (in_bounds:%s walkable:%s overlaps:%s)', log.ACTIONS.debug('Bumping %s into %s (in_bounds:%s walkable:%s overlaps:%s)',
self.actor, self.actor,
new_position, new_position,
position_is_in_bounds, position_is_in_bounds,
position_is_walkable, position_is_walkable,
entity_occupying_position) entity_occupying_position)
if not position_is_in_bounds or not position_is_walkable: if not position_is_in_bounds or not position_is_walkable:
return self.failure() return self.failure()
@ -180,7 +178,7 @@ class WalkAction(MoveAction):
def perform(self, engine: 'Engine') -> ActionResult: def perform(self, engine: 'Engine') -> ActionResult:
new_position = self.actor.position + self.direction 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 self.actor.position = new_position
return self.success() return self.success()
@ -201,13 +199,13 @@ class MeleeAction(MoveAction):
damage = self.actor.fighter.attack_power - self.target.fighter.defense damage = self.actor.fighter.attack_power - self.target.fighter.defense
if damage > 0 and self.target: 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 self.target.fighter.hit_points -= damage
else: 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: 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 ActionResult(self, alternate=DieAction(self.target))
return self.success() return self.success()
@ -216,18 +214,18 @@ class WaitAction(Action):
'''Wait a turn''' '''Wait a turn'''
def perform(self, engine: 'Engine') -> ActionResult: 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() return self.success()
class DieAction(Action): class DieAction(Action):
'''Kill an Actor''' '''Kill an Actor'''
def perform(self, engine: 'Engine') -> ActionResult: def perform(self, engine: 'Engine') -> ActionResult:
LOG.debug('%s dies', self.actor) log.ACTIONS.debug('%s dies', self.actor)
engine.entities.remove(self.actor) engine.entities.remove(self.actor)
if self.actor.yields_corpse_on_death: 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) corpse = Item(kind=items.Corpse, name=f'{self.actor.name} Corpse', position=self.actor.position)
return ActionResult(self, alternate=DropItemAction(self.actor, corpse)) return ActionResult(self, alternate=DropItemAction(self.actor, corpse))

View file

@ -1,12 +1,12 @@
# Eryn Wells <eryn@erynwells.me> # Eryn Wells <eryn@erynwells.me>
import logging
import random import random
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, List, Optional
import numpy as np import numpy as np
import tcod import tcod
from . import log
from .actions import Action, BumpAction, WaitAction from .actions import Action, BumpAction, WaitAction
from .components import Component from .components import Component
from .geometry import Direction, Point from .geometry import Direction, Point
@ -15,8 +15,6 @@ from .object import Entity
if TYPE_CHECKING: if TYPE_CHECKING:
from .engine import Engine from .engine import Engine
LOG = logging.getLogger('ai')
class AI(Component): class AI(Component):
def __init__(self, entity: Entity) -> None: def __init__(self, entity: Entity) -> None:
super().__init__() super().__init__()
@ -34,7 +32,7 @@ class HostileEnemy(AI):
radius=self.entity.sight_radius) radius=self.entity.sight_radius)
if engine.map.visible[tuple(self.entity.position)]: 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_position = engine.hero.position
hero_is_visible = visible_tiles[hero_position.x, hero_position.y] hero_is_visible = visible_tiles[hero_position.x, hero_position.y]
@ -46,13 +44,13 @@ class HostileEnemy(AI):
entity_position = self.entity.position entity_position = self.entity.position
if engine.map.visible[tuple(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 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) direction_to_next_position = entity_position.direction_to_adjacent_point(next_position)
if engine.map.visible[tuple(self.entity.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) return BumpAction(self.entity, direction_to_next_position)
else: else:
@ -68,13 +66,13 @@ class HostileEnemy(AI):
tile_is_walkable = engine.map.tile_is_walkable(new_position) tile_is_walkable = engine.map.tile_is_walkable(new_position)
if not overlaps_existing_entity and tile_is_walkable: if not overlaps_existing_entity and tile_is_walkable:
if engine.map.visible[tuple(self.entity.position)]: 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) action = BumpAction(self.entity, direction)
break break
else: else:
# If this entity somehow can't move anywhere, just wait # If this entity somehow can't move anywhere, just wait
if engine.map.visible[tuple(self.entity.position)]: 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) action = WaitAction(self.entity)
return action return action

View file

@ -2,21 +2,19 @@
'''Defines the core game engine.''' '''Defines the core game engine.'''
import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import MutableSet from typing import MutableSet
import tcod import tcod
from . import log
from . import monsters from . import monsters
from .ai import HostileEnemy from .ai import HostileEnemy
from .geometry import Size from .geometry import Size
from .map import Map from .map import Map
from .object import Entity, Hero, Monster from .object import Entity, Hero, Monster
LOG = logging.getLogger('engine')
@dataclass @dataclass
class Configuration: class Configuration:
map_size: Size map_size: Size
@ -66,7 +64,7 @@ class Engine:
else: else:
monster = Monster(monsters.Orc, ai_class=HostileEnemy, position=random_start_position) 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.entities.add(monster)
self.update_field_of_view() self.update_field_of_view()

View file

@ -2,11 +2,11 @@
'''Defines event handling mechanisms.''' '''Defines event handling mechanisms.'''
import logging
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
import tcod import tcod
from . import log
from .actions import Action, ActionResult, ExitAction, RegenerateRoomsAction, BumpAction, WaitAction from .actions import Action, ActionResult, ExitAction, RegenerateRoomsAction, BumpAction, WaitAction
from .geometry import Direction from .geometry import Direction
from .object import Actor from .object import Actor
@ -14,9 +14,6 @@ from .object import Actor
if TYPE_CHECKING: if TYPE_CHECKING:
from .engine import Engine from .engine import Engine
LOG = logging.getLogger('events')
ACTIONS_TREE_LOG = logging.getLogger('actions.tree')
class EventHandler(tcod.event.EventDispatch[Action]): class EventHandler(tcod.event.EventDispatch[Action]):
'''Handler of `tcod` events''' '''Handler of `tcod` events'''
@ -35,11 +32,11 @@ class EventHandler(tcod.event.EventDispatch[Action]):
# Unhandled event. Ignore it. # Unhandled event. Ignore it.
if not action: if not action:
LOG.debug('Unhandled event: %s', event) log.EVENTS.debug('Unhandled event: %s', event)
return return
ACTIONS_TREE_LOG.info('Processing Hero Actions') log.ACTIONS_TREE.info('Processing Hero Actions')
ACTIONS_TREE_LOG.info('|-> %s', action.actor) log.ACTIONS_TREE.info('|-> %s', action.actor)
result = self.perform_action_until_done(action) result = self.perform_action_until_done(action)
@ -54,7 +51,7 @@ class EventHandler(tcod.event.EventDispatch[Action]):
self.engine.entities, self.engine.entities,
key=lambda e: e.position.euclidean_distance_to(hero_position)) 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): for i, ent in enumerate(entities):
if not isinstance(ent, Actor): if not isinstance(ent, Actor):
@ -65,7 +62,7 @@ class EventHandler(tcod.event.EventDispatch[Action]):
continue continue
if self.engine.map.visible[tuple(ent.position)]: 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) action = ent_ai.act(self.engine)
self.perform_action_until_done(action) 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.''' '''Perform the given action and any alternate follow-up actions until the action chain is done.'''
result = action.perform(self.engine) 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: if result.alternate:
alternate_string = f'{result.alternate.__class__.__name__}[{result.alternate.actor.symbol}]' alternate_string = f'{result.alternate.__class__.__name__}[{result.alternate.actor.symbol}]'
else: else:
alternate_string = str(result.alternate) 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 '`', '|' if not result.success or not result.done else '`',
action, action,
result.success, result.success,
result.done, result.done,
alternate_string) alternate_string)
while not result.done: while not result.done:
action = result.alternate action = result.alternate
@ -94,12 +91,12 @@ class EventHandler(tcod.event.EventDispatch[Action]):
result = action.perform(self.engine) 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: if result.alternate:
alternate_string = f'{result.alternate.__class__.__name__}[{result.alternate.actor.symbol}]' alternate_string = f'{result.alternate.__class__.__name__}[{result.alternate.actor.symbol}]'
else: else:
alternate_string = str(result.alternate) 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 '`', '|' if not result.success or not result.done else '`',
action, action,
result.success, result.success,

69
erynrl/log.py Normal file
View file

@ -0,0 +1,69 @@
# Eryn Wells <eryn@erynwells.me>
'''
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)

View file

@ -1,6 +1,5 @@
# Eryn Wells <eryn@erynwells.me> # Eryn Wells <eryn@erynwells.me>
import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator, List, Optional from typing import Iterator, List, Optional
@ -8,11 +7,10 @@ from typing import Iterator, List, Optional
import numpy as np import numpy as np
import tcod import tcod
from . import log
from .geometry import Direction, Point, Rect, Size from .geometry import Direction, Point, Rect, Size
from .tile import Empty, Floor, Shroud, Wall from .tile import Empty, Floor, Shroud, Wall
LOG = logging.getLogger('map')
class Map: class Map:
def __init__(self, size: Size): def __init__(self, size: Size):
self.size = size self.size = size
@ -120,7 +118,7 @@ class RoomsAndCorridorsGenerator(MapGenerator):
node_bounds = self.__rect_from_bsp_node(node) node_bounds = self.__rect_from_bsp_node(node)
if node.children: if node.children:
LOG.debug(node_bounds) log.MAP.debug(node_bounds)
left_room: RectangularRoom = getattr(node.children[0], room_attrname) left_room: RectangularRoom = getattr(node.children[0], room_attrname)
right_room: RectangularRoom = getattr(node.children[1], room_attrname) right_room: RectangularRoom = getattr(node.children[1], room_attrname)
@ -128,8 +126,8 @@ class RoomsAndCorridorsGenerator(MapGenerator):
left_room_bounds = left_room.bounds left_room_bounds = left_room.bounds
right_room_bounds = right_room.bounds right_room_bounds = right_room.bounds
LOG.debug(' left: %s, %s', node.children[0], left_room_bounds) log.MAP.debug(' left: %s, %s', node.children[0], left_room_bounds)
LOG.debug('right: %s, %s', node.children[1], right_room_bounds) log.MAP.debug('right: %s, %s', node.children[1], right_room_bounds)
start_point = left_room_bounds.midpoint start_point = left_room_bounds.midpoint
end_point = right_room_bounds.midpoint end_point = right_room_bounds.midpoint
@ -140,16 +138,16 @@ class RoomsAndCorridorsGenerator(MapGenerator):
else: else:
corner = Point(start_point.x, end_point.y) 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.MAP.debug('Digging a tunnel between %s and %s with corner %s', start_point, end_point, corner)
LOG.debug('|-> start: %s', left_room_bounds) log.MAP.debug('|-> start: %s', left_room_bounds)
LOG.debug('`-> end: %s', right_room_bounds) log.MAP.debug('`-> end: %s', right_room_bounds)
for x, y in tcod.los.bresenham(tuple(start_point), tuple(corner)).tolist(): for x, y in tcod.los.bresenham(tuple(start_point), tuple(corner)).tolist():
tiles[x, y] = Floor tiles[x, y] = Floor
for x, y in tcod.los.bresenham(tuple(corner), tuple(end_point)).tolist(): for x, y in tcod.los.bresenham(tuple(corner), tuple(end_point)).tolist():
tiles[x, y] = Floor tiles[x, y] = Floor
else: 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 # 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 # 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))) node.y + self.rng.randint(1, max(1, node.height - size.height - 1)))
bounds = Rect(origin, size) bounds = Rect(origin, size)
LOG.debug('`-> %s', bounds) log.MAP.debug('`-> %s', bounds)
room = RectangularRoom(bounds) room = RectangularRoom(bounds)
setattr(node, room_attrname, room) setattr(node, room_attrname, room)

View file

@ -15,30 +15,30 @@
} }
}, },
"loggers": { "loggers": {
"ai": { "erynrl.ai": {
"level": "ERROR", "level": "ERROR",
"handlers": ["console"], "handlers": ["console"],
"propagate": false "propagate": false
}, },
"actions": { "erynrl.actions": {
"level": "INFO", "level": "INFO",
"handlers": ["console"] "handlers": ["console"]
}, },
"actions.movement": { "erynrl.actions.movement": {
"level": "ERROR", "level": "ERROR",
"handlers": ["console"] "handlers": ["console"]
}, },
"actions.tree": { "erynrl.actions.tree": {
"level": "INFO", "level": "INFO",
"handlers": ["console"], "handlers": ["console"],
"propagate": false "propagate": false
}, },
"events": { "erynrl.events": {
"level": "WARN", "level": "WARN",
"handlers": ["console"], "handlers": ["console"],
"propagate": false "propagate": false
}, },
"visible": { "erynrl.visible": {
"level": "DEBUG", "level": "DEBUG",
"handlers": ["console"] "handlers": ["console"]
} }