diff --git a/src/tilemapper/battlemap.py b/src/tilemapper/battlemap.py index a1945a0..c88d119 100644 --- a/src/tilemapper/battlemap.py +++ b/src/tilemapper/battlemap.py @@ -58,6 +58,12 @@ class BattleMap(BattleMapType): # invalidate the cache if hasattr(self, "grid"): del self.grid + if hasattr(self, "title"): + del self.title + 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: @@ -81,7 +87,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: @@ -101,33 +107,66 @@ class BattleMap(BattleMapType): self.height = len(matrix) return Grid(data=matrix) - @property + @cached_property def title(self) -> str: - return f"BattleMap: {self.name} ({self.width} x {self.height}, {self.width * self.height * 5}sq.ft.)" + return f"BattleMap: {self.name} ({self.width} x {self.height})" - @property + @cached_property def legend(self) -> str: output = "" - locations = 0 + locations = False for char in sorted(set(list(self.source_data)), key=str.lower): if char in self.tileset.config.legend: - if char in '0123456789': - locations = max(locations, int(char)) + if char in "0123456789": + 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 += f"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 = "" + left_coords = self.left_coordinates + i = 0 + for line in str(self).splitlines(): + lines += f"{left_coords[i].rjust(2, ' ')} │ {line}".ljust(self.width, " ") + " │\n" + i = i + 1 + top_break = "┌" + ("─" * (self.width + 2)) + "┐" + bot_break = "└" + ("─" * (self.width + 2)) + "┘" + return "\n".join( + [ + "", + self.title, + "", + indent(self.top_coordinates, " " * 5), + indent(top_break, " " * 3), + lines.rstrip(), + indent(bot_break, " " * 3), + "", + "Legend:", + indent(self.legend, " "), + ] + ) diff --git a/src/tilemapper/cli.py b/src/tilemapper/cli.py index 70de585..1aee512 100644 --- a/src/tilemapper/cli.py +++ b/src/tilemapper/cli.py @@ -71,7 +71,7 @@ def render( def render_map( source: typer.FileText = INSTALL_DIR / "examples" / "five_room_dungeon.txt", outfile: Union[Path, None] = None, - tileset: str = 'colorized' + tileset: str = "colorized", ): manager = app_state["tileset_manager"] if tileset not in manager.available: diff --git a/src/tilemapper/converter.py b/src/tilemapper/converter.py index a0f3c63..ac5fe07 100644 --- a/src/tilemapper/converter.py +++ b/src/tilemapper/converter.py @@ -1,22 +1,18 @@ -from types import SimpleNamespace -from collections import defaultdict import json +from types import SimpleNamespace terrain_map = { - "0": " ", # nothing - "16": " ", # perimeter + "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() diff --git a/src/tilemapper/grid.py b/src/tilemapper/grid.py index 2183b35..5463248 100644 --- a/src/tilemapper/grid.py +++ b/src/tilemapper/grid.py @@ -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}" diff --git a/src/tilemapper/tileset.py b/src/tilemapper/tileset.py index a8fc4b6..c5c8c68 100644 --- a/src/tilemapper/tileset.py +++ b/src/tilemapper/tileset.py @@ -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") diff --git a/src/tilesets/ascii/tileset.toml b/src/tilesets/ascii/tileset.toml index 57d5a8b..c938700 100644 --- a/src/tilesets/ascii/tileset.toml +++ b/src/tilesets/ascii/tileset.toml @@ -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" + diff --git a/test/test_mapper.py b/test/test_mapper.py index 5a44ffb..dde9b02 100644 --- a/test/test_mapper.py +++ b/test/test_mapper.py @@ -4,6 +4,7 @@ from textwrap import dedent import pytest from tilemapper import battlemap, tileset +from tilemapper.grid import X_COORDS @pytest.fixture @@ -43,4 +44,23 @@ def test_renderer(manager, sample_map): 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].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")) + 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])