From a566423e8aabf89d5a2bccc62a1632d4b99fdb23 Mon Sep 17 00:00:00 2001 From: evilchili Date: Fri, 8 Aug 2025 15:40:41 -0700 Subject: [PATCH] Initial import --- README.md | 12 +- pyproject.toml | 51 ++++ samples/edge_test.txt | 10 + samples/edge_test_small.txt | 5 + samples/five_room_dungeon.txt | 20 ++ samples/outdoor_test.txt | 10 + samples/test.txt | 7 + src/tilemapper/README.md | 0 src/tilemapper/__init__.py | 1 + src/tilemapper/battlemap.py | 94 +++++++ src/tilemapper/cli.py | 74 +++++ src/tilemapper/generator.py | 2 + src/tilemapper/grid.py | 55 ++++ src/tilemapper/tileset.py | 284 ++++++++++++++++++++ src/tilesets/test/background_1.png | Bin 0 -> 89 bytes src/tilesets/test/grass_1.png | Bin 0 -> 415 bytes src/tilesets/test/grass_2.png | Bin 0 -> 415 bytes src/tilesets/test/ground_1.png | Bin 0 -> 31239 bytes src/tilesets/test/mountain_1.png | Bin 0 -> 2827 bytes src/tilesets/test/tileset.toml | 11 + src/tilesets/test/water_1.png | Bin 0 -> 415 bytes src/tilesets/test/water_corner_ne_1.png | Bin 0 -> 243 bytes src/tilesets/test/water_corner_nw_1.png | Bin 0 -> 240 bytes src/tilesets/test/water_corner_se_1.png | Bin 0 -> 227 bytes src/tilesets/test/water_corner_sw_1.png | Bin 0 -> 237 bytes src/tilesets/test/water_edge_grass_e_1.png | Bin 0 -> 738 bytes src/tilesets/test/water_edge_grass_n_1.png | Bin 0 -> 755 bytes src/tilesets/test/water_edge_grass_ne_1.png | Bin 0 -> 1148 bytes src/tilesets/test/water_edge_grass_nw_1.png | Bin 0 -> 1114 bytes src/tilesets/test/water_edge_grass_s_1.png | Bin 0 -> 742 bytes src/tilesets/test/water_edge_grass_se_1.png | Bin 0 -> 1105 bytes src/tilesets/test/water_edge_grass_sw_1.png | Bin 0 -> 1136 bytes src/tilesets/test/water_edge_grass_w_1.png | Bin 0 -> 765 bytes test/test_mapper.py | 46 ++++ 34 files changed, 680 insertions(+), 2 deletions(-) create mode 100644 pyproject.toml create mode 100644 samples/edge_test.txt create mode 100644 samples/edge_test_small.txt create mode 100644 samples/five_room_dungeon.txt create mode 100644 samples/outdoor_test.txt create mode 100644 samples/test.txt create mode 100644 src/tilemapper/README.md create mode 100644 src/tilemapper/__init__.py create mode 100644 src/tilemapper/battlemap.py create mode 100644 src/tilemapper/cli.py create mode 100644 src/tilemapper/generator.py create mode 100644 src/tilemapper/grid.py create mode 100644 src/tilemapper/tileset.py create mode 100644 src/tilesets/test/background_1.png create mode 100644 src/tilesets/test/grass_1.png create mode 100644 src/tilesets/test/grass_2.png create mode 100644 src/tilesets/test/ground_1.png create mode 100644 src/tilesets/test/mountain_1.png create mode 100644 src/tilesets/test/tileset.toml create mode 100644 src/tilesets/test/water_1.png create mode 100644 src/tilesets/test/water_corner_ne_1.png create mode 100644 src/tilesets/test/water_corner_nw_1.png create mode 100644 src/tilesets/test/water_corner_se_1.png create mode 100644 src/tilesets/test/water_corner_sw_1.png create mode 100644 src/tilesets/test/water_edge_grass_e_1.png create mode 100644 src/tilesets/test/water_edge_grass_n_1.png create mode 100644 src/tilesets/test/water_edge_grass_ne_1.png create mode 100644 src/tilesets/test/water_edge_grass_nw_1.png create mode 100644 src/tilesets/test/water_edge_grass_s_1.png create mode 100644 src/tilesets/test/water_edge_grass_se_1.png create mode 100644 src/tilesets/test/water_edge_grass_sw_1.png create mode 100644 src/tilesets/test/water_edge_grass_w_1.png create mode 100644 test/test_mapper.py diff --git a/README.md b/README.md index 7bb0cf9..123f21c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ -# tilemapper +# TileMapper -A TTRPG battle map generator using custom tile sets. \ No newline at end of file +A TTRPG battle map generator using custom tile sets. + +## Overview + +WIP + +## Quick start + +WIP diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..42fd568 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[tool.poetry] +name = "tilemapper" +version = "0.1.0" +description = "" +authors = ["evilchili "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +typer = "^0.16.0" +pillow = "^11.3.0" + + +[tool.poetry.group.dev.dependencies] +pytest = "^8.4.1" +pytest-cov = "^6.2.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + + +### SLAM + +[tool.black] +line-length = 120 +target-version = ['py310'] + +[tool.isort] +multi_line_output = 3 +line_length = 120 +include_trailing_comma = true + +[tool.autoflake] +check = false # return error code if changes are needed +in-place = true # make changes to files instead of printing diffs +recursive = true # drill down directories recursively +remove-all-unused-imports = true # remove all unused imports (not just those from the standard library) +ignore-init-module-imports = true # exclude __init__.py when removing unused imports +remove-duplicate-keys = true # remove all duplicate keys in objects +remove-unused-variables = true # remove unused variables + +[tool.pytest.ini_options] +log_cli_level = "DEBUG" +addopts = "--cov=src --cov-report=term-missing" + +### ENDSLAM + + +[tool.poetry.scripts] +mapper = "tilemapper.cli:app" diff --git a/samples/edge_test.txt b/samples/edge_test.txt new file mode 100644 index 0000000..1ac2800 --- /dev/null +++ b/samples/edge_test.txt @@ -0,0 +1,10 @@ + ,,,,,,,, + m,__,,,_, + m____,,,, + m.__,,,,, +mm...,,,, +m,___.,,,, + m,,,,,,, + + + diff --git a/samples/edge_test_small.txt b/samples/edge_test_small.txt new file mode 100644 index 0000000..c6f7446 --- /dev/null +++ b/samples/edge_test_small.txt @@ -0,0 +1,5 @@ +,,,,,,, +,,___,, +,,___,, +,,___,, +,,,,,,, diff --git a/samples/five_room_dungeon.txt b/samples/five_room_dungeon.txt new file mode 100644 index 0000000..c8dfa55 --- /dev/null +++ b/samples/five_room_dungeon.txt @@ -0,0 +1,20 @@ +..... 1 +..... ..... 2 +..........D.... +..... ..D.. + . + . + S......... + . . + . . + ..... 3 .L.... 4 + ..... ...... + ..... ...... + ....v. + + .^......... + ........... + ........... + ........... + ........... 5 + diff --git a/samples/outdoor_test.txt b/samples/outdoor_test.txt new file mode 100644 index 0000000..4722648 --- /dev/null +++ b/samples/outdoor_test.txt @@ -0,0 +1,10 @@ + ,,,,TTTTT + ,,,.....TTT + ,,,.....TTTTT + ,,.......TTT + ,,........TT +MM._________..M +MM.________..MM +MM._____o_____..M +M__o___________.M + diff --git a/samples/test.txt b/samples/test.txt new file mode 100644 index 0000000..929b4ea --- /dev/null +++ b/samples/test.txt @@ -0,0 +1,7 @@ +,,,,,,,,,,,,,, +,,,........,,, +,,,.______.,,, +,,,._o____.,,, +,,,.______.,,, +,,,........,,, +,,,,,,,,,,,,,, diff --git a/src/tilemapper/README.md b/src/tilemapper/README.md new file mode 100644 index 0000000..e69de29 diff --git a/src/tilemapper/__init__.py b/src/tilemapper/__init__.py new file mode 100644 index 0000000..2a970bc --- /dev/null +++ b/src/tilemapper/__init__.py @@ -0,0 +1 @@ +from . import battlemap, tileset diff --git a/src/tilemapper/battlemap.py b/src/tilemapper/battlemap.py new file mode 100644 index 0000000..b954753 --- /dev/null +++ b/src/tilemapper/battlemap.py @@ -0,0 +1,94 @@ +# import logging +from dataclasses import dataclass +from functools import cached_property +from io import StringIO +from pathlib import Path +from textwrap import indent +from typing import Union + +from PIL import Image + +from tilemapper.grid import Grid, Position +from tilemapper.tileset import TileSet + + +class UnsupportedTileException(Exception): + pass + + +@dataclass +class BattleMap: + name: str = "" + source: Union[StringIO, Path] = None + source_data: str = "" + tileset: TileSet = None + width: int = 0 + height: int = 0 + + def load(self): + try: + data = self.source.read_bytes().decode() + except AttributeError: + data = self.source.read() + data = data.strip("\n") + if not self.validate_source_data(data): + return + self.source_data = data + if hasattr(self, "grid"): + del self.grid + return self.grid + + def validate_source_data(self, data: str): + for char in set(list(data)): + if char == "\n": + continue + if char not in self.tileset.character_map: + raise UnsupportedTileException(f"The current tileset does not support the '{char}' character.") + return True + + def render(self): + map_image = Image.new("RGB", (self.tileset.tile_size * self.width, self.tileset.tile_size * self.height)) + empty_space = Position(y=-1, x=-1, value=self.tileset.empty_space) + for y in range(0, self.height): + for x in range(0, self.width): + pos = self.grid.at(y, x, empty_space) + map_image.paste( + self.tileset.render_tile(pos, [a or pos for a in self.grid.adjacent(pos)]), + (self.tileset.tile_size * x, self.tileset.tile_size * y), + ) + return map_image + + @cached_property + def grid(self): + matrix = [] + for line in self.source_data.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): + return f"BattleMap: {self.name} ({self.width} x {self.height}, {self.width * self.height * 5}sq.ft.)" + + @property + def legend(self): + output = "" + for char in sorted(set(list(self.source_data)), key=str.lower): + if char in self.tileset.character_map: + output += f"{char} - {self.tileset.character_map[char]}\n" + return output + + def __str__(self): + output = "" + for y in range(0, self.height): + for x in range(0, self.width): + try: + output += str(self.grid.at(y, x).value) + except AttributeError: + continue + output += "\n" + return output.rstrip("\n") + + def __repr__(self): + return f"\n{self.title}\n\n{indent(str(self), ' ')}\n\nLegend:\n{indent(self.legend, ' ')}" diff --git a/src/tilemapper/cli.py b/src/tilemapper/cli.py new file mode 100644 index 0000000..0b306bb --- /dev/null +++ b/src/tilemapper/cli.py @@ -0,0 +1,74 @@ +import logging +import os +import sys +from pathlib import Path + +import typer +from rich.console import Console +from rich.logging import RichHandler + +from tilemapper import battlemap +from tilemapper import tileset as _tileset + +app = typer.Typer() +app_state = {} + + +@app.callback(invoke_without_command=True) +def main( + ctx: typer.Context, + config_dir: Path = Path(__file__).parent.parent / "tilesets", + verbose: bool = typer.Option(default=False, help="If True, increase verbosity of status messages."), +): + app_state["tileset_manager"] = _tileset.TileSetManager(config_dir) + app_state["config_dir"] = config_dir + app_state["verbose"] = verbose + debug = os.getenv("DEBUG", None) + logging.basicConfig( + format="%(name)s %(message)s", + level=logging.DEBUG if debug else logging.INFO, + handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[typer])], + ) + + +@app.command() +def list(): + manager = app_state["tileset_manager"] + print("\n".join(manager.available)) + + +@app.command() +def inspect(source: Path): + manager = app_state["tileset_manager"] + bmap = battlemap.BattleMap(name=source.name, source=source, tileset=manager.console_map) + bmap.load() + console = Console() + console.print(repr(bmap), highlight=False) + + +@app.command() +def render(source: Path, outfile: Path, tileset: str = typer.Option(help="The tile set to use.")): + 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}.") + + try: + bmap = battlemap.BattleMap(name=source.name, source=source, tileset=manager.load(tileset)) + except _tileset.MissingTileException as e: + logging.error(e) + sys.exit(1) + + try: + bmap.load() + except battlemap.UnsupportedTileException as e: + logging.error(e) + sys.exit(1) + + image = bmap.render() + # image = image.resize((int(0.5 * image.size[0]), int(0.5 * image.size[1]))) + image.save(outfile) + print(f"Wrote {outfile.stat().st_size} bytes to {outfile}") + + +if __name__ == "__main__": + app() diff --git a/src/tilemapper/generator.py b/src/tilemapper/generator.py new file mode 100644 index 0000000..af460c5 --- /dev/null +++ b/src/tilemapper/generator.py @@ -0,0 +1,2 @@ +def random(): + pass diff --git a/src/tilemapper/grid.py b/src/tilemapper/grid.py new file mode 100644 index 0000000..7fa8598 --- /dev/null +++ b/src/tilemapper/grid.py @@ -0,0 +1,55 @@ +from collections import namedtuple + +Position = namedtuple("GridPosition", ["y", "x", "value"]) + + +class Grid: + def __init__(self, data): + self.data = [] + for y in range(len(data)): + row = [] + for x in range(len(data[y])): + row.append(Position(y=y, x=x, value=data[y][x])) + self.data.append(row) + + def at(self, y, x, default=None): + try: + return self.data[y][x] + except IndexError: + return default + + def north(self, position: Position): + return self.at(position.y - 1, position.x) + + def east(self, position: Position): + return self.at(position.y, position.x + 1) + + def south(self, position: Position): + return self.at(position.y + 1, position.x) + + def west(self, position: Position): + return self.at(position.y, position.x - 1) + + def northwest(self, position: Position): + return self.at(position.y - 1, position.x - 1) + + def northeast(self, position: Position): + return self.at(position.y - 1, position.x + 1) + + def southwest(self, position: Position): + return self.at(position.y + 1, position.x - 1) + + def southeast(self, position: Position): + return self.at(position.y + 1, position.x + 1) + + def adjacent(self, position: Position): + return ( + self.northwest(position), + self.north(position), + self.northeast(position), + self.east(position), + self.southeast(position), + self.south(position), + self.southwest(position), + self.west(position), + ) diff --git a/src/tilemapper/tileset.py b/src/tilemapper/tileset.py new file mode 100644 index 0000000..a19c467 --- /dev/null +++ b/src/tilemapper/tileset.py @@ -0,0 +1,284 @@ +import random +import tomllib +from dataclasses import dataclass, field +from functools import cached_property +from pathlib import Path +from typing import Dict, List + +from PIL import Image, ImageDraw + +from tilemapper.grid import Position + +DEFAULT_TILE_SIZE_IN_PIXELS = 128 + +COLOR_MAP = { + ".": "white on grey58", + "d": "bold cyan on grey58", + "D": "bold green on grey58", + "L": "bold dark_red on grey58", + "S": "bold black on grey58", + "v": "bold steel_blue on grey3", + "^": "bold steel_blue on grey3", + "0": "white on dark_red", + "1": "white on dark_red", + "2": "white on dark_red", + "3": "white on dark_red", + "4": "white on dark_red", + "5": "white on dark_red", + "6": "white on dark_red", + "7": "white on dark_red", + "8": "white on dark_red", + "9": "white on dark_red", +} + + +class MissingTileException(Exception): + pass + + +@dataclass(kw_only=True) +class Tile: + name: str + char: str + + def render(self): + return str(self) + + @cached_property + def terrain_name(self): + return self.name.split("_", 1)[0] + + def __str__(self): + return self.char + + +@dataclass(kw_only=True) +class TileSet: + name: str + desc: str = "" + tiles: Dict[str, Tile] = field(default_factory=dict) + character_map: Dict[str, str] = field( + default_factory=lambda: { + " ": "empty", + ".": "ground", + "d": "door, open", + "D": "door, closed", + "L": "door, locked", + "S": "door, secret", + "v": "stairs, down", + "^": "stairs, up", + "0": "location 0", + "1": "location 1", + "2": "location 2", + "3": "location 3", + "4": "location 4", + "5": "location 5", + "6": "location 6", + "7": "location 7", + "8": "location 8", + "9": "location 9", + } + ) + + def add(self, *tiles: Tile): + for tile in tiles: + self.tiles[tile.name] = tile + + def get(self, char: str): + t = self.character_map[char] + try: + return self.tiles[f"{t}"] + except KeyError: + raise MissingTileException(f'"{self}" does not contain a "{t}" tile.') + + @property + def empty_space(self): + return self.tiles[self.character_map[" "]] + + def __str__(self): + return f"[Tileset] {self.name}: {self.desc}" + + +@dataclass +class ColorizedTile(Tile): + color: str + + def __str__(self): + return f"[{self.color}]{self.char}[/{self.color}]" + + +class ColorizedTileSet(TileSet): + tiles: Dict[str, ColorizedTile] = {} + + +@dataclass +class ImageTile(Tile): + paths: List[Path] + buffer: Image = None + + @property + def image(self): + if self.buffer: + return self.buffer + return Image.open(random.choice(self.paths)) + + @property + def width(self): + return self.image.size[0] + + @property + def height(self): + return self.image.size[1] + + def render( + self, + size: int = 0, + nw: Tile = None, + n: Tile = None, + ne: Tile = None, + e: Tile = None, + se: Tile = None, + s: Tile = None, + sw: Tile = None, + w: Tile = None, + ): + rendered = self.image.copy() + + if nw: + rendered.paste(nw.image, (0, 0)) + if n: + rendered.paste(n.image, (32, 0)) + rendered.paste(n.image, (64, 0)) + if ne: + rendered.paste(ne.image, (96, 0)) + if e: + rendered.paste(e.image, (96, 32)) + rendered.paste(e.image, (96, 64)) + if se: + rendered.paste(se.image, (96, 96)) + if s: + rendered.paste(s.image, (32, 96)) + rendered.paste(s.image, (64, 96)) + if sw: + rendered.paste(sw.image, (0, 96)) + if w: + rendered.paste(w.image, (0, 32)) + rendered.paste(w.image, (0, 64)) + + if size == rendered.width and size == rendered.height: + return rendered + return rendered.resize((size, size)) + + +@dataclass +class ImageTileSet(TileSet): + image_dir: Path + tile_size: int = 128 + + _cache = {} + + def _load_images(self, char: str, name: str): + paths = [] + lastkey = None + key = None + for imgfile in sorted(self.image_dir.glob(f"{name}_*.png")): + (terrain_name, *parts) = imgfile.stem.rsplit("_") + key = terrain_name + if parts[0] in ("edge", "corner"): + key = "_".join([terrain_name, *parts[:-1]]) + if not lastkey: + lastkey = key + if lastkey != key: + self.add(*[self._get_or_create_tile(char=char, name=lastkey, paths=paths)]) + lastkey = key + paths = [] + paths.append(imgfile) + if key: + self.add(*[self._get_or_create_tile(char=char, name=key, paths=paths)]) + + def load(self): + self.tiles = {} + for char, name in self.character_map.items(): + self._load_images(char, name) + + def _get_or_create_tile(self, char: str, name: str, paths: List[Path]): + if name in self._cache: + return self._cache[name] + buffer = None + if not paths: + buffer = Image.new("RGB", (self.tile_size, self.tile_size)) + ImageDraw.Draw(buffer).text((3, 3), name, (255, 255, 255)) + self._cache[name] = ImageTile(char=char, name=name, paths=paths, buffer=buffer) + return self._cache[name] + + def _get_overlays(self, position: Position, adjacent: List[Position] = []): + terrain = position.value.terrain_name + (nw_terrain, n_terrain, ne_terrain, e_terrain, se_terrain, s_terrain, sw_terrain, w_terrain) = [ + a.value.terrain_name for a in adjacent + ] + n = f"{terrain}_edge_{n_terrain}_n" if n_terrain != terrain else None + s = f"{terrain}_edge_{s_terrain}_s" if s_terrain != terrain else None + e = f"{terrain}_edge_{e_terrain}_e" if e_terrain != terrain else None + w = f"{terrain}_edge_{w_terrain}_w" if w_terrain != terrain else None + nw = f"{n}w" if n_terrain != terrain and w_terrain == n_terrain else n + nw = f"{terrain}_corner_nw" if n_terrain == terrain and nw_terrain != terrain else nw or w + sw = f"{s}w" if s_terrain != terrain and w_terrain == s_terrain else s + sw = f"{terrain}_corner_sw" if s_terrain == terrain and sw_terrain != terrain else sw or w + ne = f"{n}e" if n_terrain != terrain and e_terrain == n_terrain else n + ne = f"{terrain}_corner_ne" if n_terrain == terrain and ne_terrain != terrain else ne or e + se = f"{s}e" if s_terrain != terrain and e_terrain == s_terrain else s + se = f"{terrain}_corner_se" if s_terrain == terrain and se_terrain != terrain else se or e + + return (nw, n, ne, e, se, s, sw, w) + + def render_tile(self, position, adjacent): + return position.value.render( + self.tile_size, *[self._cache.get(overlay) for overlay in self._get_overlays(position, adjacent)] + ) + + +class TileSetManager: + def __init__(self, config_dir: Path): + self.config_dir = config_dir + self._available = {} + self._load_tilesets() + + @property + def available(self): + return {"console": self.console_map, "colorized": self.console_map_colorized, **self._available} + + def _load_tilesets(self): + self._available = {} + for config_file in self.config_dir.rglob("tileset.toml"): + config = tomllib.loads(config_file.read_bytes().decode()) + config["config_dir"] = config_file.parent + self._available[config["config_dir"].name] = config + + def load(self, name: str): + config = self.available[name] + tileset = ImageTileSet( + image_dir=config["config_dir"], + name=config["tileset"]["name"], + desc=config["tileset"]["desc"], + tile_size=config["tileset"].get("size", DEFAULT_TILE_SIZE_IN_PIXELS), + character_map=config["legend"], + ) + tileset.load() + return tileset + + @cached_property + def console_map(self): + ts = TileSet(name="console", desc="Tiles used for input and text rendering.") + ts.add(*[Tile(char=key, name=value) for key, value in ts.character_map.items()]) + return ts + + @cached_property + def console_map_colorized(self): + ts = ColorizedTileSet(name="colorized", desc="Colorized ASCII.") + ts.add( + *[ + ColorizedTile(char=key, name=value, color=COLOR_MAP.get(key, "grey")) + for key, value in ts.character_map.items() + ] + ) + return ts diff --git a/src/tilesets/test/background_1.png b/src/tilesets/test/background_1.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd4d5a2ca60e5f4a5a1eaaff1919666ed442d67 GIT binary patch literal 89 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}bl&H|6fVg?3rK@esza~4Y%huSQcfR&pyuh6Kh@`?fhC;RuV~076 kk9Y*q8!SeJu!n@*Q32-df{gXEfg#S|>FVdQ&MBb@0NrhXJ^%m! literal 0 HcmV?d00001 diff --git a/src/tilesets/test/ground_1.png b/src/tilesets/test/ground_1.png new file mode 100644 index 0000000000000000000000000000000000000000..11a895024b3695dba992b72b69ab9e51bc66ec6f GIT binary patch literal 31239 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 literal 0 HcmV?d00001 diff --git a/src/tilesets/test/tileset.toml b/src/tilesets/test/tileset.toml new file mode 100644 index 0000000..12dc187 --- /dev/null +++ b/src/tilesets/test/tileset.toml @@ -0,0 +1,11 @@ +[tileset] +name = "Test Tiles" +desc = "Testing edge-aware tiles" +size = 128 + +[legend] +" " = "background" +"," = "grass" +"_" = "water" +"." = "ground" +"m" = "mountain" diff --git a/src/tilesets/test/water_1.png b/src/tilesets/test/water_1.png new file mode 100644 index 0000000000000000000000000000000000000000..459c48dfbc0b3cd073d8952bf8f44b1b0605a382 GIT binary patch literal 415 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSoCO|{#S9GMf*{OzO6{{f0|R5A zr;B4qMcms<8yOoE7!DYGOnCQ1P0Z%tT(#okeLKHZo~ZiI^e(Nzg87J~!a0UQwhm*5 lIgF2Z1ky)^u!h7PcD6Pa{qCAgZ-F7s;OXk;vd$@?2>@uKf#LuF literal 0 HcmV?d00001 diff --git a/src/tilesets/test/water_corner_ne_1.png b/src/tilesets/test/water_corner_ne_1.png new file mode 100644 index 0000000000000000000000000000000000000000..3565dff6b3c7bbcc4e74e3b17c638827b9cc4e08 GIT binary patch literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3rK@es6PWWPQ z$x3<=v)Qr#JbTX6OYZxdob-e5NC_un$_MTla`*l@MtFZVap35&O1Q$fm`|egzzSwJ iS%X&%f$UHr1_qf6>KRw&&8-AFn8DN4&t;ucLK6Tz_gc&V literal 0 HcmV?d00001 diff --git a/src/tilesets/test/water_corner_nw_1.png b/src/tilesets/test/water_corner_nw_1.png new file mode 100644 index 0000000000000000000000000000000000000000..6a8cc0301440f986c1b6adbce271edf688020a22 GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3rK@es|uE3kTSLESDUD1=q#1yg9CNr|Fh=OrKTZ-z z{CW6;aK=aF1xwTSeK4r`(fm+v7UQ(5`7RtQ7iu1u4OHhQYw)TekiEw$;R@qoK8ex; eE101|3=He{DnH+EUbGMBS_V&7KbLh*2~7Yp$6J{I literal 0 HcmV?d00001 diff --git a/src/tilesets/test/water_corner_se_1.png b/src/tilesets/test/water_corner_se_1.png new file mode 100644 index 0000000000000000000000000000000000000000..b5a69d3f5a3297b6b9b9210d6e593269ea5f6f4f GIT binary patch literal 227 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3rK@es%xKL7v%lu1NER9Hu?m(6n1Fc8OAvg|l1kfgv2Z7)6Y6kK^54m=lczzgsQ z<>N$W+982y;xumJuSJ$|voT&p&2$~w}45yG3hR$cH51ba_ zj~Sf+YU5?5_I@9cxKn_r!vojqhZ&s!cH8Vri1wfbvi%<$5yMI>%v=r`BNwHl<|#_Z zfFZGx4?PXTMlo4~OHQgGo`8>4g|NGKm15ItT-ChvQ|5{87Z93}1?VcpF0ax5z0%dv zrHjFZTN2uc0&q!~K??UGf~j|Ud;qEu1CZ#0XspI?s*V6q<^z{6Z$krQF3tSO z00?}(ESN@Ac)9k##LS-z&~@%}osc%Jg8(K>b~i&2HCp!0uG)5%&JF`ukcH_2O@I~S z0S^?Un85eVw|;1MTPo;S|~4!|!0C=lT+IO_*UEbo9W3UHurP6IgikKBmR5>`Ov znPs*-4B)k{?~|4$V{?-#B{k?OkKpU(3fj}OM|0+;V+&CFKnY)0)ATS8*lNx62#KEV zphKMl7D?zTkA#N%F33gIqEpNc2k@BkEJz||Oj2%CY{psk%N;<8W8mWld) zde>?*63<|s{C2H^YQv%!fRg!M(|PXgT63r>_r7}(!Q0^jplGWWgc$d}3vBXt=gu?_ z8y~6kSrzyC+JotN;K207*qoM6N<$f|-(00{{R3 literal 0 HcmV?d00001 diff --git a/src/tilesets/test/water_edge_grass_n_1.png b/src/tilesets/test/water_edge_grass_n_1.png new file mode 100644 index 0000000000000000000000000000000000000000..bbecd277e6dd119f20f0d38e1729ec53c7402d35 GIT binary patch literal 755 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 literal 0 HcmV?d00001 diff --git a/src/tilesets/test/water_edge_grass_s_1.png b/src/tilesets/test/water_edge_grass_s_1.png new file mode 100644 index 0000000000000000000000000000000000000000..9cdc8fa44c12e7aabb7915fd9fc39ec6110ecb52 GIT binary patch literal 742 zcmV%xKL7v%m`OxIR9HvtR!fi5Kn(UIlLu*=wt_9%LpkyKb^CMpDM(!5!X1uC zthDMDvCs#wZIeu5>`}4}Y0@Mp-3xq`o78n|k3Ie-;^KXXZ~{Cq64)k2of)JS-RICG zI3u+Z$E{9?W@(-f|5gHJjqW^wW4ozrt9PD2+j(l0fGyg(uScr{LhNQ-^ImcZbicYP z-6P`S()cFNL@Sr@0--vw(x!&$NnEZno$Iu=4ezOz=!gd8Vm zLX_;=5H|=`hGJ!kRnlik!MFJE`3iXqS>FEe;U==+KEM;iBB#!tet8<+EXTm~+@qHC zN=9rzdKRD9&8O>#Ap~YF0Fe#(U_4jSjikkMLI9YJViYqXV5*+N2p@u%^ufacEo2KDu>psY z7Oz$pe-PGC4q+l6QxFN)(VSjBmhi%TsrA><4Cdh+p1IFyt|$4&V4)VUj2AEqX3z$| Y0aLUCwr4lt;s5{u07*qoM6N<$g3}XOm;e9( literal 0 HcmV?d00001 diff --git a/src/tilesets/test/water_edge_grass_se_1.png b/src/tilesets/test/water_edge_grass_se_1.png new file mode 100644 index 0000000000000000000000000000000000000000..7533a73c8d35a935f899225aa812f42bd574e6cd GIT binary patch literal 1105 zcmV-X1g`suP)%xKL7v(14%?dR9Hu~S6gozMHK#K@4I%qx!8&whc*=P#!DYs9*~fb_yGvs zkoXJzWjyhQctJo!TSQbEHKm0X(x9eJvE%z?z3bu37`vOj*s+~{tF>m&tk3yo&SlQB zJ9j;fIk1%xdL!k6jL|*ok78bD9G`7f@qyEg%^&H%;(@-8i&|3)Lvp084sv1q^w*Mn}oS3Cs#E5!FfRW|2+` z;0YFXflsT664Z#)EYdt>2Li=|rC5s*NHP?J3?!M_)cgb~(AUScVM<_G--k&F7lk2o ze!5;q61X}hHYG5xu*`!{>A2zaa7pQDGDmvwRG~=xlT?hIP z*w)HeXWLMs27?)ClLoPhq>6X$Ycbi0`W&a~G1gR9J}b)@j3TYpCDnk9Q6AULcWHMV zbWlf};uXS5D8pHvLw!&~MgND&w6<6pE{a=gt(fegeh&@0e0cIXkpRQ;X*t2J;o+kQ zfgbDNnem$(Yizvvi7r?!58fcF2Kch@T2{$09_99N%l?o8b^x1Y5MX%qE)d1&D>5!f zgz?C{i=w?P$KnvOk&)voB_ND{PAyo;@M3d8_Dw<3U|{0pE6D<(sz6mw7~e@SerWxS zO?_kftkA_6lfGV=fM{61Aj9w5z0|<%QTT@xxt3%f`m}-Vf(HW(o4{eQB&<<4jf>c)Hmeb$BOdu^VX%7l+SHXwsd{N)^BsC7^j-3=?sc~ddb z_FFPy9SS&~`>6S1wK`lSPv>Blt`@4OKzpsc#DJ7J+=0PubZHe`rHwkP;zausEjDf{ zb#B0fCC6}oFdj6LjV1(Fn`e8@$baY;a+6*Oh*Z7cSa{L{f9y1b1h%?&vP^LN(Q zxD1BO6YECVEMrf-PVM%^;&L`T!yo;pbPklTqnFX}8+g+Hoj$)d=($A3e4P0eDG%d6 XwXI;txGN~S00000NkvXXu0mjf_q`4U literal 0 HcmV?d00001 diff --git a/src/tilesets/test/water_edge_grass_sw_1.png b/src/tilesets/test/water_edge_grass_sw_1.png new file mode 100644 index 0000000000000000000000000000000000000000..5836160fef9c4ca240fd2c23797d13cb14eb5200 GIT binary patch literal 1136 zcmV-$1dscPP)%xKL7v(B1uF+R9Hu~SI=)7MHK#KcGuor|BjnDDKw}E3Y1GFREksyE^y$A zIDt5EMCw1nohwJKATDqPBH%zvDb1y7)hakv+QeSR^{)5l@Mhg4YwxaWJLzXxPG;V` z_sx56zS(8pSU1EK|2i)F?_e7L+9tj~Xu=l^A~kygM&IdqgGX>8o@G7(b=Kr>hgEoj z%`%CRMQ)zp5;LvbIh~vaysA`lCkvFu7Y$xl>(8@!q2MMjC1lN%^Q@Bn?@OBueZkI>Zoi7}bt)2c&~IIpu7TIx%@S*^g+eKNVH9@Czd!drC{A>_sGJuCZeg4t@Oc`9Q3{qofiI{-O z#&y15xDAI5Ck(C=ftL%}LNzZKvXxUJCSb4ehsTRJ+espP;OSnGWWG zK)z;-37~|dOKII`;coGd2_qT&3nDQ~=aBXs#*?&1

XVn?6^CyoB=)>eM%=q%XK8L>3v_rQ(9jpG7Wl|8IXj_F6q;S zc57I|{gEEOXGG8L7kRwdkIvWPivBM3K^0HL4np{7vhx^-J_+=A=XPYHJ?!AE(N(O6 z7jj9=3!6wvvNa;W_nj_=(IFiN6{~DBHt32DwdRQ~nA}9UXke@G6SlR#^8C&XlQOhf zbWx)$@fy`xV+Cr&5V9Y*(CBZ0ZTB|X?thfpfYKPD7k1$?2Y+i@_`&!V50vfa#hDB0 z0rp^H5FbXF1&aa&R0e|{*5oB+N5fdi*R>65%9VIgBm_Fed)N~8x-j!%$54IY(_8Qz$oB)2m9bhlc_?urhce)!qK=;v+{gtd)sz`K!ejj&9(oU{ z(fY^F6E3y0&&J2OBI>vhyoS1H!i>r=MFk#Z(q=Y_M4HLh^8_KPOATvki-M@w^*gYF z9^TO3jK_EVHacDhuj`jcj@OdQ&<(nAnc8j}i{O7WGl3eu;6Jbc0000%xKL7v%uSrBfR9Hu?S4oo7FckfgWhYJp8KA33vgswKU`KHQ&cRVwayF(F zT}2lGDvmQbo`fvNw%V<)$gaFaN)jBMCUK{)p z;S4F@K(;91K@rf$#TBq8O96C;Y>Pb+f(o%BfRW}MZDe5{kpcqp57`D-C2qY|4^r2K z@i9xG52S#5_5&q^iU-s~N3DF=oXfOJq85<+EeRkZ2Q9%v5|0qQQ|;5VL@k&jQ5q@- zsf@(3H<;jymy=dp?t#y5v?MIq4p$Ix6gH%l0Sr;nOE=N}-}aYp=n9;%Y4)($eI=n`5 zpops}?zWHu#%uz)lW7To(uh=WxK2pw%)uH$x6 zJ$IVcTQny|GsyF*m)Ltm3J66Rf~J-8bIhx2H+j?*8-EmEOi8AWbv6}Mij)G@=^7r$ v{S!(?N>!DBj5if=QmUgG$5^LaC`JAU(j(~tOn5d500000NkvXXu0mjf%IZ!u literal 0 HcmV?d00001 diff --git a/test/test_mapper.py b/test/test_mapper.py new file mode 100644 index 0000000..d4f657b --- /dev/null +++ b/test/test_mapper.py @@ -0,0 +1,46 @@ +from io import StringIO +from pathlib import Path +from textwrap import dedent + +import pytest + +from tilemapper import battlemap, tileset + + +@pytest.fixture +def manager(): + return tileset.TileSetManager(Path(__file__).parent / "fixtures") + + +@pytest.fixture +def sample_map(): + return dedent( + """ + ........ 1 + ........ ........ 2 + ........ ........ + .......L...D... D + ........ .... ... + ........ ...d ... + . + .........S. 3 + 5 . . + ....S... ...d.... 4 + ........ ........ + ........ ........ + + """ + ) + + +def test_tileset_loader(manager): + assert manager.console_map.name in manager.available + + +def test_renderer(manager, sample_map): + test_map = battlemap.BattleMap("test map", source=StringIO(sample_map), tileset=manager.console_map) + 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")