Redo the configuration metchanism

- Allow passing a font on the command line via --font
- Move the engine configuration to its own module
- Redo entirely the font configuration: move it to the configuration module
- Pass the configuration object to the Map in place of the size argument
This commit is contained in:
Eryn Wells 2023-02-12 14:24:36 -08:00
parent 6780b0495c
commit 06ae79ccd0
4 changed files with 220 additions and 57 deletions

View file

@ -1,45 +1,24 @@
# Eryn Wells <eryn@erynwells.me> # Eryn Wells <eryn@erynwells.me>
'''Main module'''
import argparse import argparse
import os.path
import sys import sys
import tcod import tcod
from . import log from . import log
from .engine import Configuration, Engine from .configuration import Configuration, FontConfiguration, FontConfigurationError, MAP_SIZE, CONSOLE_SIZE
from .geometry import Size from .engine import Engine
CONSOLE_WIDTH, CONSOLE_HEIGHT = 80, 50
MAP_WIDTH, MAP_HEIGHT = 80, 45
FONT_CP437 = 'terminal16x16_gs_ro.png'
FONT_BDF = 'ter-u32n.bdf'
def parse_args(argv, *a, **kw): def parse_args(argv, *a, **kw):
parser = argparse.ArgumentParser(*a, **kw) parser = argparse.ArgumentParser(*a, **kw)
parser.add_argument('--debug', action='store_true', default=True) parser.add_argument('--debug', action='store_true', default=True)
parser.add_argument('--font')
parser.add_argument('--sandbox', action='store_true', default=False)
args = parser.parse_args(argv) args = parser.parse_args(argv)
return args return args
def walk_up_directories_of_path(path):
while path and path != '/':
path = os.path.dirname(path)
yield path
def find_fonts_directory():
'''Walk up the filesystem tree from this script to find a 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.ROOT.info('Found fonts dir %s', possible_fonts_dir)
break
else:
return None
return possible_fonts_dir
def main(argv): def main(argv):
''' '''
Beginning of the game Beginning of the game
@ -53,25 +32,31 @@ def main(argv):
log.init() log.init()
fonts_directory = find_fonts_directory() try:
if not fonts_directory: font = args.font
log.ROOT.error("Couldn't find a fonts/ directory") if font:
font_config = FontConfiguration.with_filename(font)
else:
font_config = FontConfiguration.default_configuration()
except FontConfigurationError as error:
log.ROOT.error('Unable to create a default font configuration: %s', error)
return -1 return -1
font = os.path.join(fonts_directory, FONT_BDF) configuration = Configuration(
if not os.path.isfile(font): console_size=CONSOLE_SIZE,
log.ROOT.error("Font file %s doesn't exist", font) console_font_config=font_config,
return -1 map_size=MAP_SIZE,
sandbox=args.sandbox)
tileset = tcod.tileset.load_bdf(font)
console = tcod.Console(CONSOLE_WIDTH, CONSOLE_HEIGHT, order='F')
configuration = Configuration(map_size=Size(MAP_WIDTH, MAP_HEIGHT))
engine = Engine(configuration) engine = Engine(configuration)
tileset = configuration.console_font_config.tileset
console = tcod.Console(*configuration.console_size.numpy_shape, order='F')
with tcod.context.new(columns=console.width, rows=console.height, tileset=tileset) as context: with tcod.context.new(columns=console.width, rows=console.height, tileset=tileset) as context:
engine.run_event_loop(context, console) engine.run_event_loop(context, console)
return 0
def run_until_exit(): def run_until_exit():
''' '''

175
erynrl/configuration.py Normal file
View file

@ -0,0 +1,175 @@
# Eryn Wells <eryn@erynwells.me>
'''
Game configuration parameters.
'''
import os.path as osp
import re
from dataclasses import dataclass
from enum import Enum
from os import PathLike
from typing import Iterable
import tcod.tileset
from . import log
from .geometry import Size
CONSOLE_SIZE = Size(80, 50)
MAP_SIZE = Size(80, 45)
FONT_CP437 = 'terminal16x16_gs_ro.png'
FONT_BDF = 'ter-u32n.bdf'
class FontConfigurationError(Exception):
'''Invalid font configration based on available parameters'''
@dataclass
class FontConfiguration:
'''Configuration of the font to use for rendering the game'''
filename: str | PathLike[str]
@staticmethod
def __find_fonts_directory():
'''Walk up the filesystem tree from this file to find a `fonts` directory.'''
def walk_up_directories_of_path(path):
while path and path != '/':
path = osp.dirname(path)
yield path
for parent_dir in walk_up_directories_of_path(__file__):
possible_fonts_dir = osp.join(parent_dir, 'fonts')
if osp.isdir(possible_fonts_dir):
log.ROOT.info('Found fonts dir %s', possible_fonts_dir)
break
else:
return None
return possible_fonts_dir
@staticmethod
def default_configuration():
'''Return a default configuration: a tilesheet font configuration using `fonts/terminal16x16_gs_ro.png`.'''
fonts_directory = FontConfiguration.__find_fonts_directory()
if not fonts_directory:
message = "Couldn't find a fonts directory"
log.ROOT.error('%s', message)
raise FontConfigurationError(message)
font = osp.join(fonts_directory, 'terminal16x16_gs_ro.png')
if not osp.isfile(font):
message = f"Font file {font} doesn't exist"
log.ROOT.error("%s", message)
raise FontConfigurationError(message)
return FontConfiguration.with_filename(font)
@staticmethod
def with_filename(filename: str | PathLike[str]) -> 'FontConfiguration':
'''Return a FontConfig subclass based on the path to the filename'''
_, extension = osp.splitext(filename)
match extension:
case ".bdf":
return BDFFontConfiguration(filename)
case ".ttf":
return TTFFontConfiguration(filename)
case ".png":
# Attempt to find the tilesheet dimensions in the filename.
try:
match = re.match(r'^.*\(\d+\)x\(\d+\).*$', extension)
if not match:
return TilesheetFontConfiguration(filename)
rows, columns = int(match.group(1)), int(match.group(2))
return TilesheetFontConfiguration(
filename=filename,
dimensions=Size(columns, rows))
except ValueError:
return TilesheetFontConfiguration(filename)
case _:
raise FontConfigurationError(f'Unable to determine font configuration from {filename}')
@property
def tileset(self) -> tcod.tileset.Tileset:
'''Returns a tcod tileset based on the parameters of this font config'''
raise NotImplementedError()
@dataclass
class BDFFontConfiguration(FontConfiguration):
'''A font configuration based on a BDF file.'''
@property
def tileset(self) -> tcod.tileset.Tileset:
return tcod.tileset.load_bdf(self.filename)
@dataclass
class TTFFontConfiguration(FontConfiguration):
'''
A font configuration based on a TTF file. Since TTFs are variable width, a fixed tile size needs to be specified.
'''
tile_size: Size = Size(16, 16)
@property
def tileset(self) -> tcod.tileset.Tileset:
return tcod.tileset.load_truetype_font(self.filename, *self.tile_size)
@dataclass
class TilesheetFontConfiguration(FontConfiguration):
'''
Configuration for tilesheets. Unlike other font configurations, tilesheets must have their dimensions specified as
the number of sprites per row and number of rows.
'''
class Layout(Enum):
'''The layout of the tilesheet'''
CP437 = 1
TCOD = 2
dimensions: Size = Size(16, 16)
layout: Layout | Iterable[int] = Layout.CP437
@property
def tilesheet(self) -> Iterable[int]:
'''A tilesheet mapping for the given layout'''
if not self.layout:
return tcod.tileset.CHARMAP_CP437
if isinstance(self.layout, Iterable):
return self.layout
match self.layout:
case TilesheetFontConfiguration.Layout.CP437:
return tcod.tileset.CHARMAP_CP437
case TilesheetFontConfiguration.Layout.TCOD:
return tcod.tileset.CHARMAP_TCOD
@property
def tileset(self) -> tcod.tileset.Tileset:
'''A tcod tileset with the given parameters'''
return tcod.tileset.load_tilesheet(
self.filename,
self.dimensions.width,
self.dimensions.height,
self.tilesheet)
@dataclass
class Configuration:
'''Configuration of the game engine'''
console_size: Size
console_font_config: FontConfiguration
map_size: Size
sandbox: bool = False

View file

@ -3,7 +3,6 @@
'''Defines the core game engine.''' '''Defines the core game engine.'''
import random import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, MutableSet, NoReturn, Optional from typing import TYPE_CHECKING, List, MutableSet, NoReturn, Optional
import tcod import tcod
@ -13,8 +12,9 @@ from . import monsters
from .actions.action import Action from .actions.action import Action
from .actions.result import ActionResult from .actions.result import ActionResult
from .ai import HostileEnemy from .ai import HostileEnemy
from .configuration import Configuration
from .events import GameOverEventHandler, MainGameEventHandler from .events import GameOverEventHandler, MainGameEventHandler
from .geometry import Point, Rect, Size from .geometry import Point, Size
from .interface import Interface from .interface import Interface
from .map import Map from .map import Map
from .map.generator import RoomsAndCorridorsGenerator from .map.generator import RoomsAndCorridorsGenerator
@ -27,12 +27,6 @@ if TYPE_CHECKING:
from .events import EventHandler from .events import EventHandler
@dataclass
class Configuration:
'''Configuration of the game engine'''
map_size: Size
class Engine: class Engine:
'''The main game engine. '''The main game engine.
@ -52,8 +46,8 @@ class Engine:
A random number generator A random number generator
''' '''
def __init__(self, configuration: Configuration): def __init__(self, config: Configuration):
self.configuration = configuration self.configuration = config
self.current_turn = 1 self.current_turn = 1
self.did_begin_turn = False self.did_begin_turn = False
@ -62,9 +56,8 @@ class Engine:
self.rng = tcod.random.Random() self.rng = tcod.random.Random()
self.message_log = MessageLog() self.message_log = MessageLog()
map_size = configuration.map_size map_size = config.map_size
map_generator = RoomsAndCorridorsGenerator(BSPRoomGenerator(size=map_size), ElbowCorridorGenerator()) self.map = Map(config, map_generator)
self.map = Map(map_size, map_generator)
self.event_handler: 'EventHandler' = MainGameEventHandler(self) self.event_handler: 'EventHandler' = MainGameEventHandler(self)

View file

@ -12,29 +12,39 @@ import numpy as np
import numpy.typing as npt import numpy.typing as npt
import tcod import tcod
from ..engine import Configuration
from ..geometry import Point, Rect, Size from ..geometry import Point, Rect, Size
from .generator import MapGenerator from .generator import MapGenerator
from .tile import Empty, Shroud from .tile import Empty, Shroud
class Map: class Map:
def __init__(self, size: Size, generator: MapGenerator): def __init__(self, config: Configuration, generator: MapGenerator):
self.size = size self.configuration = config
self.tiles = np.full(tuple(size), fill_value=Empty, order='F') map_size = config.map_size
shape = tuple(map_size)
self.tiles = np.full(shape, fill_value=Empty, order='F')
generator.generate(self.tiles) generator.generate(self.tiles)
self.up_stairs = generator.up_stairs self.up_stairs = generator.up_stairs
self.down_stairs = generator.down_stairs self.down_stairs = generator.down_stairs
self.highlighted = np.full(tuple(self.size), fill_value=False, order='F') self.highlighted = np.full(shape, fill_value=False, order='F')
# Map tiles that are currently visible to the player # Map tiles that are currently visible to the player
self.visible = np.full(tuple(self.size), fill_value=False, order='F') self.visible = np.full(shape, fill_value=False, order='F')
# Map tiles that the player has explored # Map tiles that the player has explored
self.explored = np.full(tuple(self.size), fill_value=False, order='F') should_mark_all_tiles_explored = config.sandbox
self.explored = np.full(shape, fill_value=should_mark_all_tiles_explored, order='F')
self.__walkable_points = None self.__walkable_points = None
@property
def size(self) -> Size:
'''The size of the map'''
return self.configuration.map_size
def random_walkable_position(self) -> Point: def random_walkable_position(self) -> Point:
'''Return a random walkable point on the map.''' '''Return a random walkable point on the map.'''
if not self.__walkable_points: if not self.__walkable_points: