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.
This commit is contained in:
evilchili 2025-08-17 17:56:14 -07:00
parent ee8c2af2d8
commit b03f4c1193
5 changed files with 123 additions and 55 deletions

View File

@ -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 <hello@evilchi.li>"
license = "CC0"
[1]
description = "First Room"
[2]
description = "Second Room"
[3]
description = "Third Room"
[4]
description = "Forth Room"
[5]
description = "Fifth Room"

View File

@ -1,20 +0,0 @@
..... 1
..... ..... 2
..........D....
..... ..D..
.
.
S.........
. .
. .
..... 3 .L.... 4
..... ......
..... ......
....v.
.^.........
...,,,,,...
...,___,...
...,,,,,...
........... 5

View File

@ -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, " ")
]
)

View File

@ -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)

View File

@ -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)