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>
'''Main module'''
import argparse
import os.path
import sys
import tcod
from . import log
from .engine import Configuration, Engine
from .geometry import Size
CONSOLE_WIDTH, CONSOLE_HEIGHT = 80, 50
MAP_WIDTH, MAP_HEIGHT = 80, 45
FONT_CP437 = 'terminal16x16_gs_ro.png'
FONT_BDF = 'ter-u32n.bdf'
from .configuration import Configuration, FontConfiguration, FontConfigurationError, MAP_SIZE, CONSOLE_SIZE
from .engine import Engine
def parse_args(argv, *a, **kw):
parser = argparse.ArgumentParser(*a, **kw)
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)
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):
'''
Beginning of the game
@ -53,25 +32,31 @@ def main(argv):
log.init()
fonts_directory = find_fonts_directory()
if not fonts_directory:
log.ROOT.error("Couldn't find a fonts/ directory")
try:
font = args.font
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
font = os.path.join(fonts_directory, FONT_BDF)
if not os.path.isfile(font):
log.ROOT.error("Font file %s doesn't exist", font)
return -1
configuration = Configuration(
console_size=CONSOLE_SIZE,
console_font_config=font_config,
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)
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:
engine.run_event_loop(context, console)
return 0
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.'''
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, MutableSet, NoReturn, Optional
import tcod
@ -13,8 +12,9 @@ from . import monsters
from .actions.action import Action
from .actions.result import ActionResult
from .ai import HostileEnemy
from .configuration import Configuration
from .events import GameOverEventHandler, MainGameEventHandler
from .geometry import Point, Rect, Size
from .geometry import Point, Size
from .interface import Interface
from .map import Map
from .map.generator import RoomsAndCorridorsGenerator
@ -27,12 +27,6 @@ if TYPE_CHECKING:
from .events import EventHandler
@dataclass
class Configuration:
'''Configuration of the game engine'''
map_size: Size
class Engine:
'''The main game engine.
@ -52,8 +46,8 @@ class Engine:
A random number generator
'''
def __init__(self, configuration: Configuration):
self.configuration = configuration
def __init__(self, config: Configuration):
self.configuration = config
self.current_turn = 1
self.did_begin_turn = False
@ -62,9 +56,8 @@ class Engine:
self.rng = tcod.random.Random()
self.message_log = MessageLog()
map_size = configuration.map_size
map_generator = RoomsAndCorridorsGenerator(BSPRoomGenerator(size=map_size), ElbowCorridorGenerator())
self.map = Map(map_size, map_generator)
map_size = config.map_size
self.map = Map(config, map_generator)
self.event_handler: 'EventHandler' = MainGameEventHandler(self)

View file

@ -12,29 +12,39 @@ import numpy as np
import numpy.typing as npt
import tcod
from ..engine import Configuration
from ..geometry import Point, Rect, Size
from .generator import MapGenerator
from .tile import Empty, Shroud
class Map:
def __init__(self, size: Size, generator: MapGenerator):
self.size = size
def __init__(self, config: Configuration, generator: MapGenerator):
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)
self.up_stairs = generator.up_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
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
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
@property
def size(self) -> Size:
'''The size of the map'''
return self.configuration.map_size
def random_walkable_position(self) -> Point:
'''Return a random walkable point on the map.'''
if not self.__walkable_points: