From b03f4c1193f37cdd3218c3337f89aab5fb5a2456 Mon Sep 17 00:00:00 2001 From: evilchili Date: Sun, 17 Aug 2025 17:56:14 -0700 Subject: [PATCH] Source data is now in toml format The input text is now expected to be a toml file with a leading comment section defining the battle map, according to the following rules: 1. Blank lines are ignored 2. Lines at the top of the file beginning with '#', optionally with leading spaces, is treated as map data 3. Map data continues until the first line not starting with a #. 4. Any subsequent lines are treated as metadata in toml format. 5. The metadata must contain the [map] section. 6. The [map] section must contain the "name" attribute. The metadata is available as BattleMap.metadata; the map source is available as BattleMap.map_string. New convenience properties ahve been added to the BattleMap class for accessing portions of the text-only output. This includes the header and footer attributes. --- src/examples/five_room_dungeon.toml | 41 ++++++++++++++++++ src/examples/five_room_dungeon.txt | 20 --------- src/tilemapper/battlemap.py | 67 +++++++++++++++++++++-------- src/tilemapper/cli.py | 2 +- test/test_mapper.py | 48 +++++++++++++-------- 5 files changed, 123 insertions(+), 55 deletions(-) create mode 100644 src/examples/five_room_dungeon.toml delete mode 100644 src/examples/five_room_dungeon.txt diff --git a/src/examples/five_room_dungeon.toml b/src/examples/five_room_dungeon.toml new file mode 100644 index 0000000..b6719af --- /dev/null +++ b/src/examples/five_room_dungeon.toml @@ -0,0 +1,41 @@ +# ..... 1 +# ..... ..... 2 +# ..........D.... +# ..... ..D.. +# . +# . +# S......... +# . . +# . . +# ..... 3 .L.... 4 +# ..... ...... +# ..... ...... +# ....v. +# +# .^......... +# ...,,,,,... +# ...,___,... +# ...,,,,,... +# ........... 5 +# + +[map] +name = "Five Room Dungeon" +description = "An example dungeon" +author = "evilchili " +license = "CC0" + +[1] +description = "First Room" + +[2] +description = "Second Room" + +[3] +description = "Third Room" + +[4] +description = "Forth Room" + +[5] +description = "Fifth Room" diff --git a/src/examples/five_room_dungeon.txt b/src/examples/five_room_dungeon.txt deleted file mode 100644 index f446bcf..0000000 --- a/src/examples/five_room_dungeon.txt +++ /dev/null @@ -1,20 +0,0 @@ -..... 1 -..... ..... 2 -..........D.... -..... ..D.. - . - . - S......... - . . - . . - ..... 3 .L.... 4 - ..... ...... - ..... ...... - ....v. - - .^......... - ...,,,,,... - ...,___,... - ...,,,,,... - ........... 5 - diff --git a/src/tilemapper/battlemap.py b/src/tilemapper/battlemap.py index c88d119..681bec2 100644 --- a/src/tilemapper/battlemap.py +++ b/src/tilemapper/battlemap.py @@ -5,6 +5,9 @@ from io import StringIO from pathlib import Path from textwrap import indent from typing import List, Union, get_type_hints +from types import SimpleNamespace + +import tomllib from PIL import Image @@ -28,14 +31,14 @@ class BattleMap(BattleMapType): Example Usage: >>> console = TileSetManager().load("colorized") - >>> bmap = BattleMap(name="Example Map", source="input.txt", tileset=console) + >>> bmap = BattleMap("input.txt", tileset=console) >>> print(bmap.as_string()) """ - name: str = "" source: Union[StringIO, Path] = None - source_data: str = "" + map_string: str = "" + metadata: SimpleNamespace = None tileset: TileSet = None width: int = 0 height: int = 0 @@ -51,22 +54,37 @@ class BattleMap(BattleMapType): except AttributeError: data = self.source.read() data = data.strip("\n") - if not self.validate_source_data(data): - return - self.source_data = data + + map_data = "" + metadata = "" + in_map = True + for line in data.splitlines(): + line = line.lstrip(" ") + if in_map: + if line and line[0] == "#": + map_data += line[1:] + "\n" + continue + in_map = False + metadata += line + "\n" + + self.validate_map_string(map_data) + self.map_string = map_data + self.metadata = SimpleNamespace(**tomllib.loads(metadata)) # invalidate the cache if hasattr(self, "grid"): del self.grid - if hasattr(self, "title"): - del self.title + if hasattr(self, "header"): + del self.header + if hasattr(self, "footer"): + del self.footer if hasattr(self, "legend"): del self.legend if hasattr(self, "coordinates"): del self.coordinates return self.grid - def validate_source_data(self, data: str) -> bool: + def validate_map_string(self, data: str) -> bool: """ Return True if every terrain type in the input data has a corresponding tile in the current tile set. """ @@ -101,28 +119,44 @@ class BattleMap(BattleMapType): Return the current map's Grid instance. """ matrix = [] - for line in self.source_data.splitlines(): + for line in self.map_string.splitlines(): matrix.append([self.tileset.get(char) for char in line]) self.width = max([len(line) for line in matrix]) self.height = len(matrix) return Grid(data=matrix) @cached_property - def title(self) -> str: - return f"BattleMap: {self.name} ({self.width} x {self.height})" + def header(self) -> str: + lines = [f"BattleMap: {self.metadata.map['name']} ({self.width}x{self.height})"] + lines.append(('-' * len(lines[0]))) + for key, value in self.metadata.map.items(): + if key in ("name", "description"): + continue + lines.append(f"{key.title()}: {value}") + return "\n".join(lines) + + @cached_property + def footer(self) -> str: + lines = [] + if 'description' in self.metadata.map: + lines.append(f"{self.metadata.map['description']}") + lines.append("") + lines.append("Legend:") + lines.append(self.legend) + return "\n".join(lines) @cached_property def legend(self) -> str: output = "" locations = False - for char in sorted(set(list(self.source_data)), key=str.lower): + for char in sorted(set(list(self.map_string)), key=str.lower): if char in self.tileset.config.legend: if char in "0123456789": locations = True continue output += f"{char} - {self.tileset.config.legend[char]}\n" if locations: - output += f"0-9 - locations" + output += "0-9 - locations" justified = "" for line in output.splitlines(): (key, sep, desc) = line.partition(" - ") @@ -159,14 +193,13 @@ class BattleMap(BattleMapType): return "\n".join( [ "", - self.title, + self.header, "", indent(self.top_coordinates, " " * 5), indent(top_break, " " * 3), lines.rstrip(), indent(bot_break, " " * 3), "", - "Legend:", - indent(self.legend, " "), + indent(self.footer, " ") ] ) diff --git a/src/tilemapper/cli.py b/src/tilemapper/cli.py index 1aee512..dcdd604 100644 --- a/src/tilemapper/cli.py +++ b/src/tilemapper/cli.py @@ -78,7 +78,7 @@ def render_map( raise RuntimeError(f"Could not locate the tile set {tileset} in {manager.config_dir}.") try: - bmap = battlemap.BattleMap(name=source.name, source=source, tileset=manager.load(tileset)) + bmap = battlemap.BattleMap(source=source, tileset=manager.load(tileset)) bmap.load() except battlemap.UnsupportedTileException as e: logging.error(e) diff --git a/test/test_mapper.py b/test/test_mapper.py index dde9b02..6c6e081 100644 --- a/test/test_mapper.py +++ b/test/test_mapper.py @@ -16,18 +16,33 @@ def manager(): def sample_map(): return dedent( """ - ........ 1 - ........ ........ 2 - ........ ........ - .......L...D... D - ........ .... ... - ........ ...d ... - . - .........S. 3 - 5 . . - ....S... ...d.... 4 - ........ ........ - ........ ........ + # ........ 1 + # ........ ........ 2 + # ........ ........ + # .......L...D... D + # ........ .... ... + # ........ ...d ... + # . + # .........S. 3 + # 5 . . + # ....S... ...d.... 4 + # ........ ........ + # ........ ........ + + [map] + name = "sample map" + description = "sample dungeon" + + [1] + name = "room 1" + [2] + name = "room 2" + [3] + name = "room 3" + [4] + name = "room 4" + [5] + name = "room 5" """ ) @@ -39,23 +54,22 @@ def test_tileset_loader(manager): def test_renderer(manager, sample_map): - test_map = battlemap.BattleMap("test map", source=StringIO(sample_map), tileset=manager.load("ascii")) + test_map = battlemap.BattleMap(source=StringIO(sample_map), tileset=manager.load("ascii")) test_map.load() assert test_map.width == 21 assert test_map.height == 12 - assert test_map.source_data == sample_map.strip("\n") srclines = sample_map.splitlines()[1:] strlines = str(test_map).splitlines() for i in range(len(strlines)): - assert strlines[i] == srclines[i].ljust(21, " ") + assert strlines[i] == srclines[i].lstrip(' #').ljust(21, " ") def test_grid_coordiates(manager): coord_length = len(X_COORDS) map_size = 2 * coord_length + 1 - bigmap = StringIO((("." * map_size) + "\n") * map_size) - test_map = battlemap.BattleMap("test map", source=bigmap, tileset=manager.load("ascii")) + bigmap = StringIO((' # ' + ("." * map_size) + "\n") * map_size) + test_map = battlemap.BattleMap(source=bigmap, tileset=manager.load("ascii")) test_map.load() assert test_map.grid.data[-1][-1].coordinates == (f"{map_size - 1}", X_COORDS[0] * 3)