Compare commits
5
Commits
cdbb233622
...
mainline
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae3d40e91e | ||
|
|
2cddc902ab | ||
|
|
b03f4c1193 | ||
|
|
ee8c2af2d8 | ||
|
|
cc35a60210 |
@@ -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"
|
||||
@@ -1,20 +0,0 @@
|
||||
..... 1
|
||||
..... ..... 2
|
||||
..........D....
|
||||
..... ..D..
|
||||
.
|
||||
.
|
||||
S.........
|
||||
. .
|
||||
. .
|
||||
..... 3 .L.... 4
|
||||
..... ......
|
||||
..... ......
|
||||
....v.
|
||||
|
||||
.^.........
|
||||
...,,,,,...
|
||||
...,___,...
|
||||
...,,,,,...
|
||||
........... 5
|
||||
|
||||
+136
-22
@@ -1,9 +1,11 @@
|
||||
# import logging
|
||||
import tomllib
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from textwrap import indent
|
||||
from types import SimpleNamespace
|
||||
from typing import List, Union, get_type_hints
|
||||
|
||||
from PIL import Image
|
||||
@@ -28,15 +30,22 @@ class BattleMap(BattleMapType):
|
||||
Example Usage:
|
||||
|
||||
>>> console = TileSetManager().load("colorized")
|
||||
>>> bmap = BattleMap(name="Example Map", source="input.txt", tileset=console)
|
||||
>>> bmap = BattleMap("input.toml", 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
|
||||
|
||||
render_header: bool = True
|
||||
render_border: bool = True
|
||||
render_coordinates: bool = True
|
||||
render_legend: bool = True
|
||||
render_notes: bool = True
|
||||
|
||||
width: int = 0
|
||||
height: int = 0
|
||||
|
||||
@@ -51,16 +60,39 @@ 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:].lstrip() + "\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, "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
|
||||
if hasattr(self, "notes"):
|
||||
del self.notes
|
||||
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.
|
||||
"""
|
||||
@@ -81,7 +113,7 @@ class BattleMap(BattleMapType):
|
||||
Returns an Image instance.
|
||||
"""
|
||||
if get_type_hints(self.tileset.render_grid)["return"] != Image:
|
||||
raise NotImplementedError(f"Tile set does not support image rendering.")
|
||||
raise NotImplementedError("Tile set does not support image rendering.")
|
||||
return self.tileset.render_grid(grid=self.grid, width=self.width, height=self.height)
|
||||
|
||||
def as_string(self) -> str:
|
||||
@@ -95,38 +127,120 @@ 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)
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
return f"BattleMap: {self.name} ({self.width} x {self.height}, {self.width * self.height * 5}sq.ft.)"
|
||||
@cached_property
|
||||
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}")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
@property
|
||||
@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 notes(self) -> str:
|
||||
lines = []
|
||||
for number in dir(self.metadata):
|
||||
if not number.isdigit():
|
||||
continue
|
||||
location = getattr(self.metadata, number)
|
||||
lines.append(f"[{number}]{location.get('name', '')}")
|
||||
if hasattr(location, "notes"):
|
||||
for note in location.notes:
|
||||
lines.append(f" {note}")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
@cached_property
|
||||
def legend(self) -> str:
|
||||
output = ""
|
||||
locations = 0
|
||||
for char in sorted(set(list(self.source_data)), key=str.lower):
|
||||
locations = False
|
||||
for char in sorted(set(list(self.map_string)), key=str.lower):
|
||||
if char in self.tileset.config.legend:
|
||||
if char in '0123456789':
|
||||
locations = max(locations, int(char))
|
||||
if char.isdigit():
|
||||
locations = True
|
||||
continue
|
||||
output += f"{char} - {self.tileset.config.legend[char]}\n"
|
||||
if locations:
|
||||
location_key = "1" if locations == 1 else f"1-{locations}"
|
||||
output += f"{location_key} - location"
|
||||
width = len(location_key)
|
||||
output += "0-9 - locations"
|
||||
justified = ""
|
||||
for line in output.splitlines():
|
||||
(key, sep, desc) = line.partition(" - ")
|
||||
justified += f"{key.rjust(width, ' ')}{sep}{desc}\n"
|
||||
justified += f"{key.rjust(3, ' ')}{sep}{desc}\n"
|
||||
output = "".join(justified)
|
||||
return output
|
||||
|
||||
@cached_property
|
||||
def top_coordinates(self) -> str:
|
||||
row = next(row for row in self.grid.data if len(row) == self.width)
|
||||
max_len = max([len(cell.x_coordinate) for cell in row])
|
||||
lines = list(("" for i in range(max_len)))
|
||||
for pos in row:
|
||||
for i in range(len(pos.x_coordinate)):
|
||||
lines[max_len - i - 1] += pos.x_coordinate[i]
|
||||
return "\n".join((line.rjust(len(row), " ") for line in lines))
|
||||
|
||||
@cached_property
|
||||
def left_coordinates(self) -> List[str]:
|
||||
return [row[0].y_coordinate if row else "" for row in self.grid.data]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.as_string()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"\n{self.title}\n\n{indent(str(self), ' ')}\n\nLegend:\n{indent(self.legend, ' ')}"
|
||||
lines = [""]
|
||||
|
||||
map_lines = ""
|
||||
if self.render_coordinates:
|
||||
left_coords = self.left_coordinates
|
||||
i = 0
|
||||
border = "│" if self.render_border else " "
|
||||
for line in str(self).splitlines():
|
||||
map_lines += f"{left_coords[i].rjust(2, ' ')}{border}{line}".ljust(self.width, " ") + f"{border}\n"
|
||||
i = i + 1
|
||||
elif self.render_border:
|
||||
for line in str(self).splitlines():
|
||||
map_lines += f"│{line}".ljust(self.width, " ") + "│\n"
|
||||
else:
|
||||
map_lines = str(self)
|
||||
|
||||
top_break = bot_break = ""
|
||||
if self.render_border:
|
||||
top_break = "┌" + ("─" * self.width) + "┐"
|
||||
bot_break = "└" + ("─" * self.width) + "┘"
|
||||
|
||||
lines = [self.header] if self.render_header else []
|
||||
if self.render_coordinates:
|
||||
lines.append(indent(self.top_coordinates, " " * 3))
|
||||
if top_break:
|
||||
lines.append(indent(top_break, " " * 2) if self.render_coordinates else top_break)
|
||||
lines.append(map_lines.rstrip())
|
||||
if bot_break:
|
||||
lines.append(indent(bot_break, " " * 2) if self.render_coordinates else bot_break)
|
||||
lines.append("")
|
||||
|
||||
if self.render_legend:
|
||||
lines.append(indent(self.footer, " "))
|
||||
lines.append("")
|
||||
|
||||
if self.render_notes:
|
||||
lines.append(self.notes)
|
||||
|
||||
return indent("\n".join(lines), " ")
|
||||
|
||||
+33
-4
@@ -54,31 +54,60 @@ def convert(source: typer.FileText = typer.Argument(help="The donjon.sh .json fi
|
||||
@app.command()
|
||||
def render(
|
||||
source: typer.FileText = typer.Argument(
|
||||
help="The battle map text file to load.", default=INSTALL_DIR / "examples" / "five_room_dungeon.txt"
|
||||
help="The battle map text file to load.", default=INSTALL_DIR / "examples" / "five_room_dungeon.toml"
|
||||
),
|
||||
outfile: Path = typer.Option(help="The file to create. If not specified, print to STDOUT", default=None),
|
||||
tileset: str = typer.Option(
|
||||
help="The name of the tile set to use (run mapper list to see what's available).", default="colorized"
|
||||
),
|
||||
header: bool = typer.Option(help="If True, include the map header.", default=True),
|
||||
border: bool = typer.Option(help="If True, draw the map border.", default=True),
|
||||
coordinates: bool = typer.Option(help="If True, draw the coordinate outside the border.", default=True),
|
||||
legend: bool = typer.Option(help="If True, include the legend.", default=True),
|
||||
notes: bool = typer.Option(help="If True, include the notes.", default=True),
|
||||
map_only: bool = typer.Option(
|
||||
help="Equivalent to --no-header --no-border --no-coordinates --no-legend --no-notes", default=False
|
||||
),
|
||||
):
|
||||
"""
|
||||
Create a rendered battle map using a tile set. Will generate a PNG file if the tile set supports it,
|
||||
otherwise text output.
|
||||
"""
|
||||
render_map(source, outfile, tileset)
|
||||
render_map(
|
||||
source=source,
|
||||
outfile=outfile,
|
||||
tileset=tileset,
|
||||
header=header and not map_only,
|
||||
border=border and not map_only,
|
||||
coordinates=coordinates and not map_only,
|
||||
legend=legend and not map_only,
|
||||
notes=notes and not map_only,
|
||||
)
|
||||
|
||||
|
||||
def render_map(
|
||||
source: typer.FileText = INSTALL_DIR / "examples" / "five_room_dungeon.txt",
|
||||
outfile: Union[Path, None] = None,
|
||||
tileset: str = 'colorized'
|
||||
tileset: str = "colorized",
|
||||
header: bool = True,
|
||||
border: bool = True,
|
||||
coordinates: bool = True,
|
||||
legend: bool = True,
|
||||
notes: bool = True,
|
||||
):
|
||||
manager = app_state["tileset_manager"]
|
||||
if tileset not in manager.available:
|
||||
raise RuntimeError(f"Could not locate the tile set {tileset} in {manager.config_dir}.")
|
||||
|
||||
render_options = {
|
||||
"render_header": header,
|
||||
"render_border": border,
|
||||
"render_coordinates": coordinates,
|
||||
"render_notes": notes,
|
||||
"render_legend": legend,
|
||||
}
|
||||
try:
|
||||
bmap = battlemap.BattleMap(name=source.name, source=source, tileset=manager.load(tileset))
|
||||
bmap = battlemap.BattleMap(source=source, tileset=manager.load(tileset), **render_options)
|
||||
bmap.load()
|
||||
except battlemap.UnsupportedTileException as e:
|
||||
logging.error(e)
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
from types import SimpleNamespace
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
terrain_map = {
|
||||
"0": " ", # nothing
|
||||
"16": " ", # perimeter
|
||||
"4": ".",
|
||||
|
||||
"4194308": "v", # stair_down
|
||||
"8388612": "^", # stair_up
|
||||
|
||||
"13107": "d", # door
|
||||
"26214": "L", # door, locked
|
||||
"52429": "T", # door, trapped
|
||||
"10485": "S", # door, secret
|
||||
"65540": "A", # arch
|
||||
"20971": "H", # portcullis
|
||||
|
||||
"82208": "1",
|
||||
"83886": "2",
|
||||
"8556": "3",
|
||||
@@ -27,7 +23,6 @@ terrain_map = {
|
||||
"93952": "8",
|
||||
"95630": "9",
|
||||
"80530": "0",
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -39,12 +34,12 @@ def convert_donjon(source: str) -> str:
|
||||
src = SimpleNamespace(**json.loads(source))
|
||||
|
||||
textmap = ""
|
||||
for y in range(len(src.cells)):
|
||||
for y in range(1, len(src.cells) - 1):
|
||||
row = src.cells[y]
|
||||
for x in range(len(row)):
|
||||
for x in range(1, len(row) - 1):
|
||||
char = get_char(str(row[x]), default=".")
|
||||
if not char:
|
||||
raise Exception(f"{textmap}\nMissing value {row[x]} at ({y}, {x})")
|
||||
textmap += char
|
||||
textmap += "\n"
|
||||
return textmap
|
||||
return textmap.rstrip()
|
||||
|
||||
+17
-3
@@ -1,9 +1,9 @@
|
||||
from collections import namedtuple
|
||||
import string
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Union
|
||||
|
||||
# a position inside a grid.
|
||||
Position = namedtuple("Position", ["y", "x", "value"])
|
||||
Y_COORDS = string.digits
|
||||
X_COORDS = list(string.ascii_uppercase + string.ascii_lowercase)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -12,6 +12,20 @@ class Position:
|
||||
x: int
|
||||
value: Any
|
||||
|
||||
@property
|
||||
def coordinates(self) -> str:
|
||||
return (self.y_coordinate, self.x_coordinate)
|
||||
|
||||
@property
|
||||
def y_coordinate(self) -> str:
|
||||
max_coord = len(Y_COORDS)
|
||||
return str((int(self.y / max_coord) * max_coord) + (self.y % max_coord))
|
||||
|
||||
@property
|
||||
def x_coordinate(self) -> str:
|
||||
max_coord = len(X_COORDS)
|
||||
return (int(self.x / max_coord) * X_COORDS[0]) + X_COORDS[(self.x % max_coord)]
|
||||
|
||||
def __str__(self):
|
||||
return f"posiiton-{self.y}-{self.x}-{self.value}"
|
||||
|
||||
|
||||
@@ -96,6 +96,8 @@ class TileSet:
|
||||
continue
|
||||
if pos and pos.value:
|
||||
output += pos.value.render()
|
||||
else:
|
||||
output += self.empty_space.render()
|
||||
output += "\n"
|
||||
return output.rstrip("\n")
|
||||
|
||||
|
||||
@@ -8,10 +8,13 @@ class = "tilemapper.tileset.TileSet"
|
||||
"." = "ground"
|
||||
"," = "grass"
|
||||
"_" = "water"
|
||||
"A" = "archway"
|
||||
"H" = "portcullis"
|
||||
"d" = "door, open"
|
||||
"D" = "door, closed"
|
||||
"L" = "door, locked"
|
||||
"S" = "door, secret"
|
||||
"T" = "door, trapped"
|
||||
"v" = "stairs, down"
|
||||
"^" = "stairs, up"
|
||||
"0" = "location 0"
|
||||
@@ -25,3 +28,4 @@ class = "tilemapper.tileset.TileSet"
|
||||
"8" = "location 8"
|
||||
"9" = "location 9"
|
||||
|
||||
|
||||
|
||||
+53
-15
@@ -4,6 +4,7 @@ from textwrap import dedent
|
||||
import pytest
|
||||
|
||||
from tilemapper import battlemap, tileset
|
||||
from tilemapper.grid import X_COORDS
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -15,18 +16,37 @@ 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"
|
||||
description = "A large empty room."
|
||||
notes = [
|
||||
"Locked door DC 15"
|
||||
]
|
||||
[2]
|
||||
name = "room 2"
|
||||
[3]
|
||||
name = "room 3"
|
||||
[4]
|
||||
name = "room 4"
|
||||
[5]
|
||||
name = "room 5"
|
||||
|
||||
"""
|
||||
)
|
||||
@@ -38,9 +58,27 @@ 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")
|
||||
assert str(test_map) == 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].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(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)
|
||||
|
||||
lines = test_map.top_coordinates.splitlines()
|
||||
assert len(lines) == 3
|
||||
assert lines[0] == (" " * (map_size - 1)) + X_COORDS[0]
|
||||
assert lines[1][: (coord_length + 1)] == (" " * coord_length) + X_COORDS[0]
|
||||
assert lines[2] == "".join(X_COORDS) + ((coord_length + 1) * X_COORDS[0])
|
||||
|
||||
Reference in New Issue
Block a user