diff --git a/pytiled_parser/objects.py b/pytiled_parser/objects.py new file mode 100644 index 0000000..0e7667e --- /dev/null +++ b/pytiled_parser/objects.py @@ -0,0 +1,569 @@ +"""pytiled_parser objects for Tiled maps. +""" + +# pylint: disable=too-few-public-methods + +from pathlib import Path +from typing import Dict, List, NamedTuple, Optional, Union + +import attr + +# See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#data +TileLayerGrid = List[List[int]] + + +class Color(NamedTuple): + """Color object. + + Attributes: + red: Red, between 1 and 255. + green: Green, between 1 and 255. + blue: Blue, between 1 and 255. + alpha: Alpha, between 1 and 255. + """ + + red: int + green: int + blue: int + alpha: int + + +class OrderedPair(NamedTuple): + """OrderedPair NamedTuple. + + Attributes: + x: X coordinate. + y: Y coordinate. + """ + + x: Union[int, float] + y: Union[int, float] + + +class Property(NamedTuple): + """Property NamedTuple. + + Attributes: + name: Name of property + value: Value of property + """ + + name: str + value: str + + +class Size(NamedTuple): + """Size NamedTuple. + + Attributes: + width: The width of the object. + size: The height of the object. + """ + + width: Union[int, float] + height: Union[int, float] + + +@attr.s(auto_attribs=True) +class Template: + """FIXME TODO""" + + +@attr.s(auto_attribs=True) +class Chunk: + """Chunk object for infinite maps. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#chunk + + Attributes: + location: Location of chunk in tiles. + width: The width of the chunk in tiles. + height: The height of the chunk in tiles. + layer_data: The global tile IDs in chunky according to row. + """ + + location: OrderedPair + width: int + height: int + chunk_data: TileLayerGrid + + +@attr.s(auto_attribs=True) +class Image: + """Image object. + + pytiled_parser does not support embedded data in image elements at this time, + even though the TMX format technically does. + + Attributes: + source: The reference to the tileset image file. Note that this is a relative + path compared to FIXME + trans: Defines a specific color that is treated as transparent. + width: The image width in pixels (optional, used for tile index correction when + the image changes). + height: The image height in pixels (optional). + """ + + source: str + size: Optional[Size] = None + trans: Optional[str] = None + width: Optional[int] = None + height: Optional[int] = None + + +Properties = Dict[str, Union[int, float, Color, Path, str]] + + +class Grid(NamedTuple): + """Contains info for isometric maps. + + This element is only used in case of isometric orientation, and determines how tile + overlays for terrain and collision information are rendered. + + Args: + orientation: Orientation of the grid for the tiles in this tileset (orthogonal + or isometric). + width: Width of a grid cell. + height: Height of a grid cell. + """ + + orientation: str + width: int + height: int + + +class Terrain(NamedTuple): + """Terrain object. + + Args: + name: The name of the terrain type. + tile: The local tile-id of the tile that represents the terrain visually. + """ + + name: str + tile: int + + +class Frame(NamedTuple): + """Animation Frame object. + + This is only used as a part of an animation for Tile objects. + + Args: + tile_id: The local ID of a tile within the parent tile set object. + duration: How long in milliseconds this frame should be displayed before + advancing to the next frame. + """ + + tile_id: int + duration: int + + +@attr.s(auto_attribs=True) +class TileTerrain: + """Defines each corner of a tile by Terrain index in + 'TileSet.terrain_types'. + + Defaults to 'None'. 'None' means that corner has no terrain. + + Attributes: + top_left: Top left terrain type. + top_right: Top right terrain type. + bottom_left: Bottom left terrain type. + bottom_right: Bottom right terrain type. + """ + + top_left: Optional[int] = None + top_right: Optional[int] = None + bottom_left: Optional[int] = None + bottom_right: Optional[int] = None + + +@attr.s(auto_attribs=True, kw_only=True) +class Layer: + # FIXME:this docstring appears to be innacurate + """Class that all layers inherit from. + + Args: + id: Unique ID of the layer. Each layer that added to a map gets a unique id. + Even if a layer is deleted, no layer ever gets the same ID. + name: The name of the layer object. + tiled_objects: List of tiled_objects in the layer. + offset: Rendering offset of the layer object in pixels. + opacity: Decimal value between 0 and 1 to determine opacity. 1 is completely + opaque, 0 is completely transparent. + properties: Properties for the layer. + color: The color used to display the objects in this group. + draworder: Whether the objects are drawn according to the order of the object + elements in the object group element ('manual'), or sorted by their + y-coordinate ('topdown'). Defaults to 'topdown'. + See: + https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order + for more info. + + """ + + id_: int + name: str + + offset: Optional[OrderedPair] + opacity: Optional[float] + properties: Optional[Properties] + + +LayerData = Union[TileLayerGrid, List[Chunk]] +"""The tile data for one layer. + +Either a 2 dimensional array of integers representing the global tile IDs + for a TileLayerGrid, or a list of chunks for an infinite map layer. +""" + + +@attr.s(auto_attribs=True, kw_only=True) +class TileLayer(Layer): + """Tile map layer containing tiles. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#layer + + Args: + size: The width of the layer in tiles. The same as the map width unless map is + infitite. + data: Either an 2 dimensional array of integers representing the global tile + IDs for the map layer, or a list of chunks for an infinite map. + """ + + size: Size + layer_data: LayerData + + +@attr.s(auto_attribs=True, kw_only=True) +class TiledObject: + """TiledObject object. + + See: + https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object + + Args: + id: Unique ID of the object. Each object that is placed on a map gets a unique + id. Even if an object was deleted, no object gets the same ID. + gid: Global tiled object ID. + location: The location of the object in pixels. + size: The width of the object in pixels (default: (0, 0)). + rotation: The rotation of the object in degrees clockwise (default: 0). + opacity: The opacity of the object. (default: 255) + name: The name of the object. + type: The type of the object. + properties: The properties of the TiledObject. + template: A reference to a Template object FIXME + """ + + id_: int + gid: Optional[int] = None + + location: OrderedPair + size: Optional[Size] = None + rotation: Optional[float] = None + opacity: Optional[float] = None + + name: Optional[str] = None + type: Optional[str] = None + + properties: Optional[Properties] = None + template: Optional[Template] = None + + +@attr.s() +class RectangleObject(TiledObject): + """Rectangle shape defined by a point, width, and height. + + See: https://doc.mapeditor.org/en/stable/manual/objects/#insert-rectangle + (objects in tiled are rectangles by default, so there is no specific + documentation on the tmx-map-format page for it.) + """ + + +@attr.s() +class ElipseObject(TiledObject): + """Elipse shape defined by a point, width, and height. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#ellipse + """ + + +@attr.s() +class PointObject(TiledObject): + """Point defined by a point (x,y). + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#point + """ + + +@attr.s(auto_attribs=True, kw_only=True) +class TileImageObject(TiledObject): + """Polygon shape defined by a set of connections between points. + + See: https://doc.mapeditor.org/en/stable/manual/objects/#insert-tile + + Attributes: + gid: Refference to a global tile id. + """ + + gid: int + + +@attr.s(auto_attribs=True, kw_only=True) +class PolygonObject(TiledObject): + """Polygon shape defined by a set of connections between points. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polygon + + Attributes: + points: FIXME + """ + + points: List[OrderedPair] + + +@attr.s(auto_attribs=True, kw_only=True) +class PolylineObject(TiledObject): + """Polyline defined by a set of connections between points. + + See: + https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline + + Attributes: + points: List of coordinates relative to the location of the object. + """ + + points: List[OrderedPair] + + +@attr.s(auto_attribs=True, kw_only=True) +class TextObject(TiledObject): + """Text object with associated settings. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#text + and https://doc.mapeditor.org/en/stable/manual/objects/#insert-text + + Attributes: + font_family: The font family used (default: "sans-serif") + font_size: The size of the font in pixels. (default: 16) + wrap: Whether word wrapping is enabled. (default: False) + color: Color of the text. (default: #000000) + bold: Whether the font is bold. (default: False) + italic: Whether the font is italic. (default: False) + underline: Whether the text is underlined. (default: False) + strike_out: Whether the text is striked-out. (default: False) + kerning: Whether kerning should be used while rendering the text. (default: + False) + horizontal_align: Horizontal alignment of the text (default: "left") + vertical_align: Vertical alignment of the text (defalt: "top") + """ + + text: str + font_family: str = "sans-serif" + font_size: int = 16 + wrap: bool = False + color: str = "#000000" + bold: bool = False + italic: bool = False + underline: bool = False + strike_out: bool = False + kerning: bool = False + horizontal_align: str = "left" + vertical_align: str = "top" + + +@attr.s(auto_attribs=True, kw_only=True) +class ObjectLayer(Layer): + """ + TiledObject Group Object. + + The object group is in fact a map layer, and is hence called "object layer" in + Tiled. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup + + Args: + tiled_objects: List of tiled_objects in the layer. + offset: Rendering offset of the layer object in pixels. + color: The color used to display the objects in this group. FIXME: editor only? + draworder: Whether the objects are drawn according to the order of the object + elements in the object group element ('manual'), or sorted by their + y-coordinate ('topdown'). Defaults to 'topdown'. See: + https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order + for more info. + + """ + + tiled_objects: List[TiledObject] + + color: Optional[str] = None + draw_order: Optional[str] = "topdown" + + +@attr.s(auto_attribs=True, kw_only=True) +class LayerGroup(Layer): + """Layer Group. + + A LayerGroup can be thought of as a layer that contains layers + (potentially including other LayerGroups). + + Offset and opacity recursively affect child layers. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#group + + Attributes: + Layers: Layers in group. + """ + + layers: Optional[List[Union["LayerGroup", Layer, ObjectLayer]]] + + +@attr.s(auto_attribs=True, kw_only=True) +class ImageLayer(Layer): + """Image Layer. + + An image layer displays a single image. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#imagelayer + + Attributes: + image: the image to display for this layer. + """ + + image: Image + + +@attr.s(auto_attribs=True) +class TileSet: + """Object for storing a TSX with all associated collision data. + + Args: + name: The name of this tileset. + max_tile_size: The maximum size of a tile in this tile set in pixels. + spacing: The spacing in pixels between the tiles in this tileset (applies to + the tileset image). + margin: The margin around the tiles in this tileset (applies to the tileset + image). + tile_count: The number of tiles in this tileset. + columns: The number of tile columns in the tileset. For image collection + tilesets it is editable and is used when displaying the tileset. + grid: Only used in case of isometric orientation, and determines how tile + overlays for terrain and collision information are rendered. + tileoffset: Used to specify an offset in pixels when drawing a tile from the + tileset. When not present, no offset is applied. + image: Used for spritesheet tile sets. + terrain_types: List of of terrain types which can be referenced from the + terrain attribute of the tile object. Ordered according to the terrain + element's appearance in the TSX file. + tiles: Dict of Tile objects by Tile.id. + tsx_file: Path of the file containing the tileset, None if loaded internally + from a map + parent_dir: Path of the parent directory of the file containing the tileset, + None if loaded internally from a map + """ + + name: str + max_tile_size: Size + + spacing: Optional[int] = None + margin: Optional[int] = None + tile_count: Optional[int] = None + columns: Optional[int] = None + tile_offset: Optional[OrderedPair] = None + grid: Optional[Grid] = None + properties: Optional[Properties] = None + image: Optional[Image] = None + terrain_types: Optional[List[Terrain]] = None + tiles: Optional[Dict[int, "Tile"]] = None + tsx_file: Path = None + parent_dir: Path = None + + +TileSetDict = Dict[int, TileSet] + + +@attr.s(auto_attribs=True, kw_only=True) +class Tile: + """Individual tile object. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tile + + Args: + id: The local tile ID within its tileset. + type: The type of the tile. Refers to an object type and is used by tile + objects. + terrain: Defines the terrain type of each corner of the tile. + animation: Each tile can have exactly one animation associated with it. + """ + + id_: int + type_: Optional[str] = None + terrain: Optional[TileTerrain] = None + animation: Optional[List[Frame]] = None + objectgroup: Optional[List[TiledObject]] = None + image: Optional[Image] = None + properties: Optional[List[Property]] = None + tileset: Optional[TileSet] = None + flipped_horizontally: bool = False + flipped_diagonally: bool = False + flipped_vertically: bool = False + + +@attr.s(auto_attribs=True) +class TileMap: + """Object for storing a TMX with all associated layers and properties. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map + + Attributes: + parent_dir: The directory the TMX file is in. Used for finding relative paths + to TSX files and other assets. + version: The TMX format version. + tiledversion: The Tiled version used to save the file. May be a date (for + snapshot builds). + orientation: Map orientation. Tiled supports "orthogonal", "isometric", + "staggered" and "hexagonal" + renderorder: The order in which tiles on tile layers are rendered. Valid values + are right-down, right-up, left-down and left-up. In all cases, the map is + drawn row-by-row. (only supported for orthogonal maps at the moment) + map_size: The map width in tiles. + tile_size: The width of a tile. + infinite: If the map is infinite or not. + hexsidelength: Only for hexagonal maps. Determines the width or height + (depending on the staggered axis) of the tile's edge, in pixels. + stagger_axis: For staggered and hexagonal maps, determines which axis ("x" or + "y") is staggered. + staggerindex: For staggered and hexagonal maps, determines whether the "even" + or "odd" indexes along the staggered axis are shifted. + backgroundcolor: The background color of the map. + nextlayerid: Stores the next available ID for new layers. + nextobjectid: Stores the next available ID for new objects. + tile_sets: Dict of tile sets used in this map. Key is the first GID for the + tile set. The value is a TileSet object. + layers: List of layer objects by draw order. + """ + + parent_dir: Path + tmx_file: Union[str, Path] + + version: str + tiled_version: str + orientation: str + render_order: str + map_size: Size + tile_size: Size + infinite: bool + next_layer_id: Optional[int] + next_object_id: int + + tile_sets: TileSetDict + layers: List[Layer] + + hex_side_length: Optional[int] = None + stagger_axis: Optional[int] = None + stagger_index: Optional[int] = None + background_color: Optional[Color] = None + + properties: Optional[Properties] = None diff --git a/pytiled_parser/typing_helpers.py b/pytiled_parser/typing_helpers.py new file mode 100644 index 0000000..cc13d40 --- /dev/null +++ b/pytiled_parser/typing_helpers.py @@ -0,0 +1,6 @@ +def is_float(string: str): + try: + float(string) + return True + except (ValueError, TypeError): + return False diff --git a/pytiled_parser/utilities.py b/pytiled_parser/utilities.py new file mode 100644 index 0000000..7d5e09c --- /dev/null +++ b/pytiled_parser/utilities.py @@ -0,0 +1,69 @@ +"""Helper unitilies for pytiled_parser.""" + +from typing import List, Optional + +import pytiled_parser.objects as objects + + +def parse_color(color: str) -> objects.Color: + """Convert Tiled color format into Arcade's. + + Args: + color (str): Tiled formatted color string. + + Returns: + :Color: Color object in the format that Arcade understands. + """ + # the actual part we care about is always an even number + if len(color) % 2: + # strip initial '#' character + color = color[1:] + + if len(color) == 6: + # full opacity if no alpha specified + alpha = 0xFF + red = int(color[0:2], 16) + green = int(color[2:4], 16) + blue = int(color[4:6], 16) + else: + alpha = int(color[0:2], 16) + red = int(color[2:4], 16) + green = int(color[4:6], 16) + blue = int(color[6:8], 16) + + return objects.Color(red, green, blue, alpha) + + +def _get_tile_set_key(gid: int, tile_set_keys: List[int]) -> int: + """Gets tile set key given a tile GID. + + Args: + gid (int): Global ID of the tile. + tile_set_keys (List[int]): List of tile set keys. + + Returns: + int: The key of the tile set that contains the tile for the GID. + """ + + # credit to __m4ch1n3__ on ##learnpython for this idea + return max([key for key in tile_set_keys if key <= gid]) + + +def get_tile_by_gid(gid: int, tile_sets: objects.TileSetDict) -> Optional[objects.Tile]: + """Gets correct Tile for a given global ID. + + Args: + tile_sets (objects.TileSetDict): TileSetDict from TileMap. + gid (int): Global tile ID of the tile to be returned. + + Returns: + objects.Tile: The Tile object reffered to by the global tile ID. + None: If there is no objects.Tile object in the tile_set.tiles dict for the associated gid. + """ + tile_set_key = _get_tile_set_key(gid, list(tile_sets.keys())) + tile_set = tile_sets[tile_set_key] + + if tile_set.tiles is not None: + return tile_set.tiles.get(gid - tile_set_key) + + return None diff --git a/pytiled_parser/xml_parser.py b/pytiled_parser/xml_parser.py new file mode 100644 index 0000000..d03fed7 --- /dev/null +++ b/pytiled_parser/xml_parser.py @@ -0,0 +1,940 @@ +"""Handle XML parsing for TMX files""" + +import base64 +import functools +import gzip +import re +import xml.etree.ElementTree as etree +import zlib +from pathlib import Path +from typing import Callable, Dict, List, Optional, Tuple, Union + +import pytiled_parser.objects as objects +import pytiled_parser.typing_helpers as TH +from pytiled_parser.utilities import parse_color + + +def _decode_base64_data( + data_text: str, layer_width: int, compression: Optional[str] = None +) -> objects.TileLayerGrid: + """Decode base64 data. + + Args: + data_text: Data to be decoded. + layer_width: Width of each layer in tiles. + compression: The type of compression for the data. + + Raises: + ValueError: If compression type is unsupported. + + Returns: + objects.TileLayerGrid: Tile grid. + """ + tile_grid: objects.TileLayerGrid = [[]] + + unencoded_data = base64.b64decode(data_text) + if compression == "zlib": + unzipped_data = zlib.decompress(unencoded_data) + elif compression == "gzip": + unzipped_data = gzip.decompress(unencoded_data) + elif compression is None: + unzipped_data = unencoded_data + else: + raise ValueError(f"Unsupported compression type: '{compression}'.") + + # Turn bytes into 4-byte integers + byte_count = 0 + int_count = 0 + int_value = 0 + row_count = 0 + for byte in unzipped_data: + int_value += byte << (byte_count * 8) + byte_count += 1 + if not byte_count % 4: + byte_count = 0 + int_count += 1 + tile_grid[row_count].append(int_value) + int_value = 0 + if not int_count % layer_width: + row_count += 1 + tile_grid.append([]) + + tile_grid.pop() + return tile_grid + + +def _decode_csv_data(data_text: str) -> objects.TileLayerGrid: + """Decodes csv encoded layer data. + + Args: + data_text: Data to be decoded. + + Returns: + objects.TileLayerGrid: Tile grid. + """ + tile_grid = [] + lines: List[str] = data_text.split("\n") + # remove erroneous empty lists due to a newline being on both ends of text + lines = lines[1:-1] + for line in lines: + line_list = line.split(",") + # FIXME: what is this for? + while "" in line_list: + line_list.remove("") + line_list_int = [int(item) for item in line_list] + tile_grid.append(line_list_int) + + return tile_grid + + +# I'm not sure if this is the best way to do this +TileLayerDecoder = Union[ + Callable[[str], objects.TileLayerGrid], + Callable[[str, int, Optional[str]], objects.TileLayerGrid], +] + + +def _decode_tile_layer_data( + element: etree.Element, + layer_width: int, + encoding: str, + compression: Optional[str] = None, +) -> objects.TileLayerGrid: + """Decodes tile layer data or chunk data. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tmx-data + + Args: + element: Element to have text decoded. + layer_width: Number of tiles per column in this layer. Used for determining + when to cut off a row when decoding base64 encoding layers. + encoding: Encoding format of the layer data. + compression: Compression format of the layer data. + + Raises: + ValueError: Encoding type is not supported. + + Returns: + objects.TileLayerGrid: Tile grid. + """ + + data_text: str = element.text # type: ignore + + if encoding == "csv": + return _decode_csv_data(data_text) + if encoding == "base64": + return _decode_base64_data(data_text, layer_width, compression) + + raise ValueError(f"{encoding} is not a supported encoding") + + +def _parse_data(element: etree.Element, layer_width: int) -> objects.LayerData: + """Parses layer data. + + Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data. + + Args: + element: Data element to parse. + layer_width: Layer width. Used for base64 decoding. + + Returns: + LayerData: Data object containing layer data or chunks of data. + """ + encoding = element.attrib["encoding"] + + compression = None + try: + compression = element.attrib["compression"] + except KeyError: + pass + + chunk_elements = element.findall("./chunk") + if chunk_elements: + chunks: List[objects.Chunk] = [] + for chunk_element in chunk_elements: + x = int(chunk_element.attrib["x"]) + y = int(chunk_element.attrib["y"]) + location = objects.OrderedPair(x, y) + width = int(chunk_element.attrib["width"]) + height = int(chunk_element.attrib["height"]) + layer_data = _decode_tile_layer_data( + chunk_element, layer_width, encoding, compression + ) + chunks.append(objects.Chunk(location, width, height, layer_data)) + return chunks + + return _decode_tile_layer_data(element, layer_width, encoding, compression) + + +def _parse_layer( + layer_element: etree.Element, +) -> Tuple[ + int, + str, + Optional[objects.OrderedPair], + Optional[float], + Optional[objects.Properties], +]: + """Parses all of the attributes for a Layer object. + + Args: + layer_element: The layer element to be parsed. + + Returns: + FIXME + """ + id_ = int(layer_element.attrib["id"]) + + name = layer_element.attrib["name"] + + offset: Optional[objects.OrderedPair] + offset_x = layer_element.attrib.get("offsetx") + offset_y = layer_element.attrib.get("offsety") + if offset_x and offset_y: + assert TH.is_float(offset_x) + assert TH.is_float(offset_y) + offset = objects.OrderedPair(float(offset_x), float(offset_y)) + else: + offset = None + + opacity: Optional[float] + opacity_attrib = layer_element.attrib.get("opacity") + if opacity_attrib: + opacity = float(opacity_attrib) + else: + opacity = None + + properties: Optional[objects.Properties] + properties_element = layer_element.find("./properties") + if properties_element is not None: + properties = _parse_properties_element(properties_element) + else: + properties = None + + return id_, name, offset, opacity, properties + + +def _parse_tile_layer(element: etree.Element,) -> objects.TileLayer: + """Parses tile layer element. + + Args: + element: The layer element to be parsed. + + Raises: + ValueError: Element has no child data element. + + Returns: + TileLayer: The tile layer object. + """ + id_, name, offset, opacity, properties = _parse_layer(element) + + width = int(element.attrib["width"]) + height = int(element.attrib["height"]) + size = objects.Size(width, height) + + data_element = element.find("./data") + assert data_element is not None + layer_data: objects.LayerData = _parse_data(data_element, width) + + return objects.TileLayer( + id_=id_, + name=name, + offset=offset, + opacity=opacity, + properties=properties, + size=size, + layer_data=layer_data, + ) + + +def _parse_tiled_objects( + object_elements: List[etree.Element], +) -> List[objects.TiledObject]: + """Parses objects found in a 'objectgroup' element. + + Args: + object_elements: List of object elements to be parsed. + + Returns: + list: List of parsed tiled objects. + """ + tiled_objects: List[objects.TiledObject] = [] + + for object_element in object_elements: + my_object = _parse_object(object_element) + if my_object is not None: + tiled_objects.append(my_object) + + return tiled_objects + + +def _parse_object(obj): + my_id = obj.attrib["id"] + my_x = float(obj.attrib["x"]) + my_y = float(obj.attrib["y"]) + my_location = objects.OrderedPair(x=my_x, y=my_y) + if "width" in obj.attrib: + my_width = float(obj.attrib["width"]) + else: + my_width = None + if "height" in obj.attrib: + my_height = float(obj.attrib["height"]) + else: + my_height = None + my_name = obj.attrib["name"] + + properties: Optional[objects.Properties] + properties_element = obj.find("./properties") + if properties_element is not None: + properties = _parse_properties_element(properties_element) + else: + properties = None + + # This is where it would be nice if we could assume a walrus + # operator was part of our Python distribution. + + my_object = None + + polygon = obj.findall("./polygon") + + if polygon and len(polygon) > 0: + points = _parse_points(polygon[0].attrib["points"]) + my_object = objects.PolygonObject( + id_=my_id, + name=my_name, + location=my_location, + size=(my_width, my_height), + points=points, + properties=properties, + ) + + if my_object is None: + polyline = obj.findall("./polyline") + + if polyline and len(polyline) > 0: + points = _parse_points(polyline[0].attrib["points"]) + my_object = objects.PolylineObject( + id_=my_id, + name=my_name, + location=my_location, + size=(my_width, my_height), + points=points, + properties=properties, + ) + + if my_object is None: + ellipse = obj.findall("./ellipse") + + if ellipse and len(ellipse): + my_object = objects.ElipseObject( + id_=my_id, + name=my_name, + location=my_location, + size=(my_width, my_height), + properties=properties, + ) + + if my_object is None: + if "template" in obj.attrib: + print( + "Warning, this .tmx file is using an unsupported" + "'template' attribute. Ignoring." + ) + + if my_object is None: + point = obj.findall("./point") + if point: + my_object = objects.PointObject( + id_=my_id, + name=my_name, + location=my_location, + properties=properties, + ) + + if my_object is None: + my_object = objects.RectangleObject( + id_=my_id, + name=my_name, + location=my_location, + size=(my_width, my_height), + properties=properties, + ) + + return my_object + + + +def _parse_object_layer(element: etree.Element,) -> objects.ObjectLayer: + """Parse the objectgroup element given. + + See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup + + Args: + element: Element to be parsed. + + Returns: + ObjectLayer: The object layer object. + """ + id_, name, offset, opacity, properties = _parse_layer(element) + + color = None + try: + color = element.attrib["color"] + except KeyError: + pass + + draw_order = None + try: + draw_order = element.attrib["draworder"] + except KeyError: + pass + + tiled_objects = _parse_tiled_objects(element.findall("./object")) + + return objects.ObjectLayer( + id_=id_, + name=name, + offset=offset, + opacity=opacity, + properties=properties, + color=color, + draw_order=draw_order, + tiled_objects=tiled_objects, + ) + + +def _parse_image_layer(element: etree.Element,) -> objects.ImageLayer: + """Parse the imagelayer element given. + + Args: + element: Element to be parsed. + + Returns: + ImageLayer: The image layer object. + """ + id_, name, offset, opacity, properties = _parse_layer(element) + + image = _parse_image_element(element.find("./image")) + + return objects.ImageLayer( + id_=id_, + name=name, + offset=offset, + opacity=opacity, + properties=properties, + image=image, + ) + + +def _parse_layer_group(element: etree.Element,) -> objects.LayerGroup: + """Parse the objectgroup element given. + + Args: + element: Element to be parsed. + + Returns: + LayerGroup: The layer group object. + """ + id_, name, offset, opacity, properties = _parse_layer(element) + + layers = _get_layers(element) + + return objects.LayerGroup( + id_=id_, + name=name, + offset=offset, + opacity=opacity, + properties=properties, + layers=layers, + ) + + +def _get_layer_parser( + layer_tag: str, +) -> Optional[Callable[[etree.Element], objects.Layer]]: + """Gets a the parser for the layer type specified. + + Layer tags are 'layer' for a tile layer, 'objectgroup' for an object layer, and + 'group' for a layer group. If anything else is passed, returns None. + + Args: + layer_tag: Specifies the layer type to be parsed based on the element tag. + + Returns: + Callable: the function to be used to parse the layer. + None: The element is not a map layer. + """ + if layer_tag == "layer": + return _parse_tile_layer + if layer_tag == "objectgroup": + return _parse_object_layer + if layer_tag == "group": + return _parse_layer_group + if layer_tag == "imagelayer": + return _parse_image_layer + return None + + +def _get_layers(map_element: etree.Element) -> List[objects.Layer]: + """Parse layer type element given. + + Retains draw order based on the returned lists index FIXME: confirm + + Args: + map_element: The element containing the layer. + + Returns: + List[Layer]: A list of the layers, ordered by draw order. FIXME: confirm + """ + layers: List[objects.Layer] = [] + for element in map_element.findall("./"): + layer_parser = _get_layer_parser(element.tag) + if layer_parser: + layers.append(layer_parser(element)) + + return layers + + +@functools.lru_cache() +def _parse_external_tile_set( + parent_dir: Path, tile_set_element: etree.Element +) -> objects.TileSet: + """Parses an external tile set. + + Caches the results to speed up subsequent maps with identical tilesets. + + Args: + parent_dir: Directory that TMX is in. + tile_set_element: Tile set element. + + Returns: + objects.Tileset: The tileset being parsed. + """ + source = Path(tile_set_element.attrib["source"]) + resolved_path = parent_dir / source + tile_set_tree = etree.parse(str(parent_dir / Path(source))).getroot() + + parsed_tile_set = _parse_tile_set(tile_set_tree) + + parsed_tile_set.tsx_file = resolved_path + parsed_tile_set.parent_dir = resolved_path.parent + + return parsed_tile_set + + +def _parse_points(point_string: str) -> List[objects.OrderedPair]: + str_pairs = point_string.split(" ") + + points = [] + for str_pair in str_pairs: + xys = str_pair.split(",") + x = float(xys[0]) + y = float(xys[1]) + points.append(objects.OrderedPair(x, y)) + + return points + + +def _parse_tiles(tile_element_list: List[etree.Element]) -> Dict[int, objects.Tile]: + """Parse a list of tile elements. + + Args: + tile_element_list: List of tile elements. + + Returns: + Dict[int, objects.Tile]: Dictionary containing Tile objects by their ID. + """ + tiles: Dict[int, objects.Tile] = {} + for tile_element in tile_element_list: + # id is not optional + id_ = int(tile_element.attrib["id"]) + + # optional attributes + _type = None + try: + _type = tile_element.attrib["type"] + except KeyError: + pass + + terrain = None + try: + tile_terrain_attrib = tile_element.attrib["terrain"] + except KeyError: + pass + else: + # below is an attempt to explain how terrains are handled. + # 'terrain' attribute is a comma seperated list of 4 values, + # each is either an integer or blank + + # convert to list of values + terrain_list_attrib = re.split(",", tile_terrain_attrib) + # terrain_list is list of indexes of Tileset.terrain_types + terrain_list: List[Optional[int]] = [] + # each index in terrain_list_attrib refers to a corner + for corner in terrain_list_attrib: + if not corner: + terrain_list.append(None) + else: + terrain_list.append(int(corner)) + terrain = objects.TileTerrain(*terrain_list) + + # tile element optional sub-elements + properties: Optional[List[objects.Property]] = None + tile_properties_element = tile_element.find("./properties") + if tile_properties_element: + properties = [] + property_list = tile_properties_element.findall("./property") + for property_ in property_list: + name = property_.attrib["name"] + value = property_.attrib["value"] + obj = objects.Property(name, value) + properties.append(obj) + + # tile element optional sub-elements + animation: Optional[List[objects.Frame]] = None + tile_animation_element = tile_element.find("./animation") + if tile_animation_element: + animation = [] + frames = tile_animation_element.findall("./frame") + for frame in frames: + # tileid refers to the Tile.id of the animation frame + animated_id = int(frame.attrib["tileid"]) + # duration is in MS. Should perhaps be converted to seconds. + # FIXME: make decision + duration = int(frame.attrib["duration"]) + animation.append(objects.Frame(animated_id, duration)) + + # tile element optional sub-elements + objectgroup: Optional[List[objects.TiledObject]] = None + objectgroup_element = tile_element.find("./objectgroup") + if objectgroup_element: + objectgroup = [] + object_list = objectgroup_element.findall("./object") + for obj in object_list: + my_object = _parse_object(obj) + if my_object is not None: + objectgroup.append(my_object) + + # if this is None, then the Tile is part of a spritesheet + image = None + image_element = tile_element.find("./image") + if image_element is not None: + image = _parse_image_element(image_element) + + # print(f"Adding '{id_}', {image}, {objectgroup}") + + tiles[id_] = objects.Tile( + id_=id_, + type_=_type, + terrain=terrain, + animation=animation, + image=image, + properties=properties, + tileset=None, + objectgroup=objectgroup, + ) + + return tiles + + +def _parse_image_element(image_element: etree.Element) -> objects.Image: + """Parse image element given. + + Args: + image_element (etree.Element): Image element to be parsed. + + Returns: + objects.Image: FIXME what is this? + """ + # FIXME doc + image = objects.Image(image_element.attrib["source"]) + + width_attrib = image_element.attrib.get("width") + height_attrib = image_element.attrib.get("height") + + if width_attrib and height_attrib: + image.size = objects.Size(int(width_attrib), int(height_attrib)) + + try: + image.trans = image_element.attrib["trans"] + except KeyError: + pass + + return image + + +def _parse_properties_element(properties_element: etree.Element) -> objects.Properties: + # FIXME: wtf is this pseudo 'attributes' section? + """Adds Tiled property to Properties dict. + + Each property element has a number of attributes: + name: Name of property. + property_type: Type of property. Can be string, int, float, bool, color or + file. Defaults to string. + value: The value of the property. + + Args: + properties_element: Element to be parsed. + + Returns: + objects.Properties: Dict of the property values by property name. + + + """ + properties: objects.Properties = {} + for property_element in properties_element.findall("./property"): + name = property_element.attrib["name"] + try: + property_type = property_element.attrib["type"] + except KeyError: + # strings do not have an attribute in property elements + property_type = "string" + value = property_element.attrib["value"] + + property_types = ["string", "int", "float", "bool", "color", "file"] + assert property_type in property_types, f"Invalid type for property {name}" + + if property_type == "int": + properties[name] = int(value) + elif property_type == "float": + properties[name] = float(value) + elif property_type == "color": + properties[name] = value + elif property_type == "file": + properties[name] = Path(value) + elif property_type == "bool": + if value == "true": + properties[name] = True + else: + properties[name] = False + else: + properties[name] = value + + return properties + + +def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet: + """Parses a tile set that is embedded into a TMX. + + Args: + tile_set_element: Element to be parsed. + + Returns: + objects.TileSet: Tile Set from element. + """ + # get all basic attributes + name = tile_set_element.attrib["name"] + max_tile_width = int(tile_set_element.attrib["tilewidth"]) + max_tile_height = int(tile_set_element.attrib["tileheight"]) + max_tile_size = objects.Size(max_tile_width, max_tile_height) + + spacing = None + try: + spacing = int(tile_set_element.attrib["spacing"]) + except KeyError: + pass + + margin = None + try: + margin = int(tile_set_element.attrib["margin"]) + except KeyError: + pass + + tile_count = None + try: + tile_count = int(tile_set_element.attrib["tilecount"]) + except KeyError: + pass + + columns = None + try: + columns = int(tile_set_element.attrib["columns"]) + except KeyError: + pass + + tile_offset = None + tileoffset_element = tile_set_element.find("./tileoffset") + if tileoffset_element is not None: + tile_offset_x = int(tileoffset_element.attrib["x"]) + tile_offset_y = int(tileoffset_element.attrib["y"]) + tile_offset = objects.OrderedPair(tile_offset_x, tile_offset_y) + + grid = None + grid_element = tile_set_element.find("./grid") + if grid_element is not None: + grid_orientation = grid_element.attrib["orientation"] + grid_width = int(grid_element.attrib["width"]) + grid_height = int(grid_element.attrib["height"]) + grid = objects.Grid(grid_orientation, grid_width, grid_height) + + properties = None + properties_element = tile_set_element.find("./properties") + if properties_element is not None: + properties = _parse_properties_element(properties_element) + + terrain_types: Optional[List[objects.Terrain]] = None + terrain_types_element = tile_set_element.find("./terraintypes") + if terrain_types_element is not None: + terrain_types = [] + for terrain in terrain_types_element.findall("./terrain"): + name = terrain.attrib["name"] + terrain_tile = int(terrain.attrib["tile"]) + terrain_types.append(objects.Terrain(name, terrain_tile)) + + image = None + image_element = tile_set_element.find("./image") + if image_element is not None: + image = _parse_image_element(image_element) + + tile_element_list = tile_set_element.findall("./tile") + tiles = _parse_tiles(tile_element_list) + + tileset = objects.TileSet( + name, + max_tile_size, + spacing, + margin, + tile_count, + columns, + tile_offset, + grid, + properties, + image, + terrain_types, + tiles, + ) + + # Go back and create a circular link so tiles know what tileset they are + # part of. Needed for animation. + for id_, tile in tiles.items(): + tile.tileset = tileset + + return tileset + + +def _get_tile_sets(map_element: etree.Element, parent_dir: Path) -> objects.TileSetDict: + """Get tile sets. + + Args: + map_element: Element to be parsed. + parent_dir: Directory that TMX is in. + + Returns: + objects.TileSetDict: Dict of tile sets in the TMX by first_gid + """ + # parse all tilesets + tile_sets: objects.TileSetDict = {} + tile_set_element_list = map_element.findall("./tileset") + for tile_set_element in tile_set_element_list: + # tiled docs are ambiguous about the 'firstgid' attribute + # current understanding is for the purposes of mapping the layer + # data to the tile set data, add the 'firstgid' value to each + # tile 'id'; this means that the 'firstgid' is specific to each, + # tile set as they pertain to the map, not tile set specific as + # the tiled docs can make it seem + # 'firstgid' the key for each TileMap + first_gid = int(tile_set_element.attrib["firstgid"]) + try: + # check if is an external TSX + source = tile_set_element.attrib["source"] + except KeyError: + # the tile set is embedded + name = tile_set_element.attrib["name"] + tile_sets[first_gid] = _parse_tile_set(tile_set_element) + else: + # tile set is external + tile_sets[first_gid] = _parse_external_tile_set( + parent_dir, tile_set_element + ) + + return tile_sets + + +def parse_tile_map(tmx_file: Union[str, Path]) -> objects.TileMap: + """Parse tile map. + + Args: + tmx_file: TMX file to be parsed. + + Returns: + objects.TileMap: TileMap object generated from the TMX file provided. + """ + # setting up XML parsing + map_tree = etree.parse(str(tmx_file)) + map_element = map_tree.getroot() + + # positional arguments for TileMap + parent_dir = Path(tmx_file).parent + + version = map_element.attrib["version"] + tiled_version = map_element.attrib["tiledversion"] + orientation = map_element.attrib["orientation"] + render_order = map_element.attrib["renderorder"] + + map_width = int(map_element.attrib["width"]) + map_height = int(map_element.attrib["height"]) + map_size = objects.Size(map_width, map_height) + + tile_width = int(map_element.attrib["tilewidth"]) + tile_height = int(map_element.attrib["tileheight"]) + tile_size = objects.Size(tile_width, tile_height) + + infinite_attribute = map_element.attrib["infinite"] + infinite = bool(infinite_attribute == "1") + + if "nextlayerid" in map_element.attrib: + next_layer_id = int(map_element.attrib["nextlayerid"]) + else: + next_layer_id = None + + if "nextobjectid" in map_element.attrib: + next_object_id = int(map_element.attrib["nextobjectid"]) + else: + next_object_id = None + + tile_sets = _get_tile_sets(map_element, parent_dir) + + layers = _get_layers(map_element) + + tile_map = objects.TileMap( + parent_dir, + tmx_file, + version, + tiled_version, + orientation, + render_order, + map_size, + tile_size, + infinite, + next_layer_id, + next_object_id, + tile_sets, + layers, + ) + + try: + tile_map.hex_side_length = int(map_element.attrib["hexsidelength"]) + except KeyError: + pass + + try: + tile_map.stagger_axis = map_element.attrib["staggeraxis"] + except KeyError: + pass + + try: + tile_map.stagger_index = map_element.attrib["staggerindex"] + except KeyError: + pass + + try: + color = parse_color(map_element.attrib["backgroundcolor"]) + tile_map.background_color = (color.red, color.green, color.blue) + except KeyError: + pass + + properties_element = map_tree.find("./properties") + if properties_element is not None: + tile_map.properties = _parse_properties_element(properties_element) + + return tile_map diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..3aeeee8 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.main(["--tb=native", "-s", "tests"]) diff --git a/tests/test_data/test_map_image_tile_set.tmx b/tests/test_data/test_map_image_tile_set.tmx new file mode 100644 index 0000000..63e60f8 --- /dev/null +++ b/tests/test_data/test_map_image_tile_set.tmx @@ -0,0 +1,91 @@ + + + + + +1,2,3,4,5,6,7,8,30,30, +9,10,11,12,13,14,15,16,30,30, +17,18,19,20,21,22,23,24,30,30, +25,26,27,28,29,30,31,32,30,30, +33,34,35,36,37,38,39,40,30,30, +41,42,43,44,45,46,47,48,30,30, +30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30 + + + + +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,46,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,6,7,7,7,7,7,8,0, +0,0,14,15,15,15,15,15,16,0, +0,0,22,23,23,23,23,23,24,0 + + + + + + + + +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,31,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0 + + + + +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0, +1,2,3,0,0,0,0,0,0,0, +9,10,11,0,0,0,0,0,0,0, +17,18,19,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + Hello World + + + + + + + + + + + + + + diff --git a/tests/test_data/test_map_infinite.tmx b/tests/test_data/test_map_infinite.tmx new file mode 100644 index 0000000..d117bcf --- /dev/null +++ b/tests/test_data/test_map_infinite.tmx @@ -0,0 +1,390 @@ + + + + + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,1,2,3, +30,30,30,30,30,30,30,30,30,30,30,30,30,9,10,11, +30,30,30,30,30,30,30,30,30,30,30,30,30,17,18,19 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +4,5,6,7,8,30,30,30,30,30,30,30,30,30,30,30, +12,13,14,15,16,30,30,30,30,30,30,30,30,30,30,30, +20,21,22,23,24,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,25,26,27, +30,30,30,30,30,30,30,30,30,30,30,30,30,33,34,35, +30,30,30,30,30,30,30,30,30,30,30,30,30,41,42,43, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +28,29,30,31,32,30,30,30,30,30,30,30,30,30,30,30, +36,37,38,39,40,30,30,30,30,30,30,30,30,30,30,30, +44,45,46,47,48,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30, +30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30 + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,21, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,29, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + +0,0,20,21,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,28,29,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +20,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +20,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +28,29,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + +28,29,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + diff --git a/tests/test_data/test_map_simple.tmx b/tests/test_data/test_map_simple.tmx new file mode 100644 index 0000000..f5e2a0d --- /dev/null +++ b/tests/test_data/test_map_simple.tmx @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + +1,2,3,4,5,6,7,8, +9,10,11,12,13,14,15,16, +17,18,19,20,21,22,23,24, +25,26,27,28,29,30,31,32, +33,34,35,36,37,38,39,40, +41,42,43,44,45,46,47,48 + + + diff --git a/tests/test_data/test_map_simple_meme.tmx b/tests/test_data/test_map_simple_meme.tmx new file mode 100644 index 0000000..bc47bdc --- /dev/null +++ b/tests/test_data/test_map_simple_meme.tmx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + H4sIAAAAAAAAAw3DBRKCQAAAwDMRA7BQLMTE9v+vY3dmWyGEth279uwbOTB26MixExNTM6fOnLtwae7KtYUbt+7ce7D0aOXJsxev3rxb+/Dpy7cfv/782wAcvDirwAAAAA== + + + diff --git a/tests/test_data/test_map_simple_objects.tmx b/tests/test_data/test_map_simple_objects.tmx new file mode 100644 index 0000000..26adfbb --- /dev/null +++ b/tests/test_data/test_map_simple_objects.tmx @@ -0,0 +1,14 @@ + + + + + +1,2,3,4,5,6,7,8, +9,10,11,12,13,14,15,16, +17,18,19,20,21,22,23,24, +25,26,27,28,29,30,31,32, +33,34,35,36,37,38,39,40, +41,42,43,44,45,46,47,48 + + + diff --git a/tests/test_data/test_map_simple_offset.tmx b/tests/test_data/test_map_simple_offset.tmx new file mode 100644 index 0000000..400a639 --- /dev/null +++ b/tests/test_data/test_map_simple_offset.tmx @@ -0,0 +1,22 @@ + + + + + + + + + + + + + +1,2,3,4,5,6,7,8, +9,10,11,12,13,14,15,16, +17,18,19,20,21,22,23,24, +25,26,27,28,29,30,31,32, +33,34,35,36,37,38,39,40, +41,42,43,44,45,46,47,48 + + + diff --git a/tests/test_data/tile_set_image.tsx b/tests/test_data/tile_set_image.tsx new file mode 100644 index 0000000..2f6ef20 --- /dev/null +++ b/tests/test_data/tile_set_image.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/tile_set_image_objects.tsx b/tests/test_data/tile_set_image_objects.tsx new file mode 100644 index 0000000..6d65375 --- /dev/null +++ b/tests/test_data/tile_set_image_objects.tsx @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_infinite_maps.py b/tests/test_infinite_maps.py new file mode 100644 index 0000000..33f5d1b --- /dev/null +++ b/tests/test_infinite_maps.py @@ -0,0 +1,82 @@ +"""Tests for infinite maps""" +import os +import xml.etree.ElementTree as etree +from contextlib import ExitStack as does_not_raise +from pathlib import Path + +import pytest + +import pytiled_parser +from pytiled_parser import objects, utilities, xml_parser + +TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) +TEST_DATA = TESTS_DIR / "test_data" + + +def test_map_infinite(): + """ + TMX with a very simple spritesheet tile set and some properties. + """ + + test_map = pytiled_parser.parse_tile_map(TEST_DATA / "test_map_infinite.tmx") + + print(test_map.layers) + + # map + assert test_map.version == "1.2" + assert test_map.tiled_version == "1.2.3" + assert test_map.orientation == "orthogonal" + assert test_map.render_order == "right-down" + assert test_map.map_size == (8, 6) + assert test_map.tile_size == (32, 32) + assert test_map.infinite + assert test_map.next_layer_id == 3 + assert test_map.next_object_id == 1 + + # optional, not for orthogonal maps + assert test_map.hex_side_length == None + assert test_map.stagger_axis == None + assert test_map.stagger_index == None + assert test_map.background_color == None + + # tileset + assert test_map.tile_sets[1].name == "tile_set_image" + assert test_map.tile_sets[1].max_tile_size == (32, 32) + assert test_map.tile_sets[1].spacing == 1 + assert test_map.tile_sets[1].margin == 1 + assert test_map.tile_sets[1].tile_count == 48 + assert test_map.tile_sets[1].columns == 8 + assert test_map.tile_sets[1].tile_offset == None + assert test_map.tile_sets[1].grid == None + assert test_map.tile_sets[1].properties == None + + # unsure how to get paths to compare propperly + assert str(test_map.tile_sets[1].image.source) == ("images/tmw_desert_spacing.png") + assert test_map.tile_sets[1].image.trans == None + assert test_map.tile_sets[1].image.size == (265, 199) + + assert test_map.tile_sets[1].terrain_types == None + assert test_map.tile_sets[1].tiles == {} + + # layers + assert test_map.layers[0].id_ == 1 + assert test_map.layers[0].name == "Tile Layer 1" + assert test_map.layers[0].offset == None + assert test_map.layers[0].opacity == None + assert test_map.layers[0].properties == None + assert test_map.layers[0].size == (8, 6) + + # fmt: off + assert test_map.layers[0].layer_data == [objects.Chunk(location=objects.OrderedPair(x=-32, y=-32), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=-32), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=0, y=-32), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=16, y=-32), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-32, y=-16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=-16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 1, 2, 3], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 9, 10, 11], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 17, 18, 19]]), objects.Chunk(location=objects.OrderedPair(x=0, y=-16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [4, 5, 6, 7, 8, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [12, 13, 14, 15, 16, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [20, 21, 22, 23, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=16, y=-16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-32, y=0), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=0), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 25, 26, 27], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 33, 34, 35], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 41, 42, 43], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=0, y=0), width=16, height=16, chunk_data=[[28, 29, 30, 31, 32, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [36, 37, 38, 39, 40, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [44, 45, 46, 47, 48, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=16, y=0), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-32, y=16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=0, y=16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=16, y=16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]])] + # fmt: on + + assert test_map.layers[1].id_ == 2 + assert test_map.layers[1].name == "Tile Layer 2" + assert test_map.layers[1].offset == None + assert test_map.layers[1].opacity == None + assert test_map.layers[1].properties == None + assert test_map.layers[1].size == (8, 6) + + # fmt: off + assert test_map.layers[1].layer_data == [objects.Chunk(location=objects.OrderedPair(x=-32, y=-16), width=16, height=16, chunk_data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 21], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 29], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), objects.Chunk(location=objects.OrderedPair(x=16, y=-16), width=16, height=16, chunk_data=[[0, 0, 20, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=0), width=16, height=16, chunk_data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [20, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), objects.Chunk(location=objects.OrderedPair(x=16, y=0), width=16, height=16, chunk_data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [20, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=16), width=16, height=16, chunk_data=[[28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])] + # fmt: on diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..dcad131 --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,223 @@ +"""Unit tests for pytiled_parser""" + +import xml.etree.ElementTree as etree +from contextlib import ExitStack as does_not_raise + +import pytest + +from pytiled_parser import objects, utilities, xml_parser + +LAYER_DATA = [ + ( + '' + "", + (int(1), "Tile Layer 1", None, None, None), + ), + ( + '' + + "", + (int(2), "Tile Layer 2", None, float(0.5), None), + ), + ( + '' + + "" + + "" + + "", + (int(5), "Tile Layer 4", objects.OrderedPair(49, -50), None, "properties",), + ), +] + + +@pytest.mark.parametrize("xml,expected", LAYER_DATA) +def test_parse_layer(xml, expected, monkeypatch): + def mockreturn(*args): + return "properties" + + monkeypatch.setattr(xml_parser, "_parse_properties_element", mockreturn) + + result = xml_parser._parse_layer(etree.fromstring(xml)) + + assert result == expected + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("#001122", (0x00, 0x11, 0x22, 0xFF)), + ("001122", (0x00, 0x11, 0x22, 0xFF)), + ("#FF001122", (0x00, 0x11, 0x22, 0xFF)), + ("FF001122", (0x00, 0x11, 0x22, 0xFF)), + ("FF001122", (0x00, 0x11, 0x22, 0xFF)), + ], +) +def test_color_parsing(test_input, expected): + assert utilities.parse_color(test_input) == expected + + +layer_data = [ + ( + etree.fromstring( + "\n1,2,3,4,5,6,7,8,\n" + "9,10,11,12,13,14,15,16,\n" + "17,18,19,20,21,22,23,24,\n" + "25,26,27,28,29,30,31,32,\n" + "33,34,35,36,37,38,39,40,\n" + "41,42,43,44,45,46,47,48\n" + ), + 8, + "csv", + None, + [ + [1, 2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31, 32], + [33, 34, 35, 36, 37, 38, 39, 40], + [41, 42, 43, 44, 45, 46, 47, 48], + ], + does_not_raise(), + ), + ( + etree.fromstring("\n0,0,0,0,0\n"), + 5, + "csv", + None, + [[0, 0, 0, 0, 0]], + does_not_raise(), + ), + ( + etree.fromstring( + "AQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAEAAAABEAAAASAAAAEwAAABQAAAAVAAAAFgAAABcAAAAYAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAACAAAAAhAAAAIgAAACMAAAAkAAAAJQAAACYAAAAnAAAAKAAAACkAAAAqAAAAKwAAACwAAAAtAAAALgAAAC8AAAAwAAAA" + ), + 8, + "base64", + None, + [ + [1, 2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31, 32], + [33, 34, 35, 36, 37, 38, 39, 40], + [41, 42, 43, 44, 45, 46, 47, 48], + ], + does_not_raise(), + ), + ( + etree.fromstring( + "eJwNwwUSgkAAAMAzEQOwUCzExPb/r2N3ZlshhLYdu/bsGzkwdujIsRMTUzOnzpy7cGnuyrWFG7fu3Huw9GjlybMXr968W/vw6cu3H7/+/NsAMw8EmQ==" + ), + 8, + "base64", + "zlib", + [ + [1, 2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31, 32], + [33, 34, 35, 36, 37, 38, 39, 40], + [41, 42, 43, 44, 45, 46, 47, 48], + ], + does_not_raise(), + ), + ( + etree.fromstring( + "H4sIAAAAAAAAAw3DBRKCQAAAwDMRA7BQLMTE9v+vY3dmWyGEth279uwbOTB26MixExNTM6fOnLtwae7KtYUbt+7ce7D0aOXJsxev3rxb+/Dpy7cfv/782wAcvDirwAAAAA==" + ), + 8, + "base64", + "gzip", + [ + [1, 2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31, 32], + [33, 34, 35, 36, 37, 38, 39, 40], + [41, 42, 43, 44, 45, 46, 47, 48], + ], + does_not_raise(), + ), + ( + etree.fromstring("SGVsbG8gV29ybGQh"), + 8, + "base64", + "lzma", + [ + [1, 2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31, 32], + [33, 34, 35, 36, 37, 38, 39, 40], + [41, 42, 43, 44, 45, 46, 47, 48], + ], + pytest.raises(ValueError), + ), + ( + etree.fromstring( + "/ .---- --..-- ..--- --..-- ...-- --..-- ....- --..-- ..... --..-- -.... --..-- --... --..-- ---.. --..-- / ----. --..-- .---- ----- --..-- .---- .---- --..-- .---- ..--- --..-- .---- ...-- --..-- .---- ....- --..-- .---- ..... --..-- .---- -.... --..-- / .---- --... --..-- .---- ---.. --..-- .---- ----. --..-- ..--- ----- --..-- ..--- .---- --..-- ..--- ..--- --..-- ..--- ...-- --..-- ..--- ....- --..-- / ..--- ..... --..-- ..--- -.... --..-- ..--- --... --..-- ..--- ---.. --..-- ..--- ----. --..-- ...-- ----- --..-- ...-- .---- --..-- ...-- ..--- --..-- / ...-- ...-- --..-- ...-- ....- --..-- ...-- ..... --..-- ...-- -.... --..-- ...-- --... --..-- ...-- ---.. --..-- ...-- ----. --..-- ....- ----- --..-- / ....- .---- --..-- ....- ..--- --..-- ....- ...-- --..-- ....- ....- --..-- ....- ..... --..-- ....- -.... --..-- ....- --... --..-- ....- ---.." + ), + 8, + "morse", + None, + [ + [1, 2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31, 32], + [33, 34, 35, 36, 37, 38, 39, 40], + [41, 42, 43, 44, 45, 46, 47, 48], + ], + pytest.raises(ValueError), + ), +] + + +@pytest.mark.parametrize( + "layer_data,width,encoding,compression,expected,raises", layer_data +) +def test_decode_layer_data(layer_data, width, encoding, compression, expected, raises): + with raises: + assert ( + xml_parser._decode_tile_layer_data(layer_data, width, encoding, compression) + == expected + ) + + +# FIXME: use hypothesis for this +def create_tile_set(qty_of_tiles): + """ Create tile set of specific size. + """ + tile_set = objects.TileSet(None, None) + + if qty_of_tiles == 0: + return tile_set + + tiles = {} + + for tile_id in range(qty_of_tiles): + tiles[tile_id] = objects.Tile(id_=tile_id) + + tile_set.tiles = tiles + + return tile_set + + +tile_by_gid = [ + (1, {1: create_tile_set(0)}, None), + (1, {1: create_tile_set(1)}, objects.Tile(id_=0)), + (1, {1: create_tile_set(2)}, objects.Tile(id_=0)), + (2, {1: create_tile_set(1)}, None), + (10, {1: create_tile_set(10)}, objects.Tile(id_=9)), + (1, {1: create_tile_set(1), 2: create_tile_set(1)}, objects.Tile(id_=0)), + (2, {1: create_tile_set(1), 2: create_tile_set(1)}, objects.Tile(id_=0)), + (3, {1: create_tile_set(1), 2: create_tile_set(1)}, None), + (15, {1: create_tile_set(5), 6: create_tile_set(10)}, objects.Tile(id_=9)), + ( + 20, + {1: create_tile_set(5), 6: create_tile_set(10), 16: create_tile_set(10),}, + objects.Tile(id_=4), + ), +] + + +@pytest.mark.parametrize("gid,tile_sets,expected", tile_by_gid) +def test_get_tile_by_gid(gid, tile_sets, expected): + assert utilities.get_tile_by_gid(gid, tile_sets) == expected diff --git a/tests/test_pytiled_parser_integration.py b/tests/test_pytiled_parser_integration.py new file mode 100644 index 0000000..8022cde --- /dev/null +++ b/tests/test_pytiled_parser_integration.py @@ -0,0 +1,98 @@ +import os +from pathlib import Path + +import pytest + +import pytiled_parser + +TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) +TEST_DATA = TESTS_DIR / "test_data" + + +def test_map_simple(): + """ + TMX with a very simple spritesheet tile set and some properties. + """ + + test_map = pytiled_parser.parse_tile_map(TEST_DATA / "test_map_simple.tmx") + + # map + # unsure how to get paths to compare propperly + assert test_map.parent_dir == TEST_DATA + assert test_map.version == "1.2" + assert test_map.tiled_version == "1.2.3" + assert test_map.orientation == "orthogonal" + assert test_map.render_order == "right-down" + assert test_map.map_size == (8, 6) + assert test_map.tile_size == (32, 32) + assert test_map.infinite == False + assert test_map.next_layer_id == 2 + assert test_map.next_object_id == 1 + + # optional, not for orthogonal maps + assert test_map.hex_side_length == None + assert test_map.stagger_axis == None + assert test_map.stagger_index == None + assert test_map.background_color == None + + assert test_map.properties == { + "bool property - false": False, + "bool property - true": True, + "color property": "#ff49fcff", + "file property": Path("/var/log/syslog"), + "float property": 1.23456789, + "int property": 13, + "string property": "Hello, World!!", + } + + # tileset + assert test_map.tile_sets[1].name == "tile_set_image" + assert test_map.tile_sets[1].max_tile_size == (32, 32) + assert test_map.tile_sets[1].spacing == 1 + assert test_map.tile_sets[1].margin == 1 + assert test_map.tile_sets[1].tile_count == 48 + assert test_map.tile_sets[1].columns == 8 + assert test_map.tile_sets[1].tile_offset == None + assert test_map.tile_sets[1].grid == None + assert test_map.tile_sets[1].properties == None + + # unsure how to get paths to compare propperly + assert str(test_map.tile_sets[1].image.source) == ("images/tmw_desert_spacing.png") + assert test_map.tile_sets[1].image.trans == None + assert test_map.tile_sets[1].image.size == (265, 199) + + assert test_map.tile_sets[1].terrain_types == None + assert test_map.tile_sets[1].tiles == {} + + # layers + assert test_map.layers[0].layer_data == [ + [1, 2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31, 32], + [33, 34, 35, 36, 37, 38, 39, 40], + [41, 42, 43, 44, 45, 46, 47, 48], + ] + assert test_map.layers[0].id_ == 1 + assert test_map.layers[0].name == "Tile Layer 1" + assert test_map.layers[0].offset == None + assert test_map.layers[0].opacity == None + assert test_map.layers[0].properties == None + assert test_map.layers[0].size == (8, 6) + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("#001122", (0x00, 0x11, 0x22, 0xFF)), + ("001122", (0x00, 0x11, 0x22, 0xFF)), + ("#FF001122", (0x00, 0x11, 0x22, 0xFF)), + ("FF001122", (0x00, 0x11, 0x22, 0xFF)), + ("FF001122", (0x00, 0x11, 0x22, 0xFF)), + ], +) +def test_color_parsing(test_input, expected): + """ + Tiled has a few different types of color representations. + """ + assert pytiled_parser.utilities.parse_color(test_input) == expected diff --git a/tests/test_test_map_simple_offset.py b/tests/test_test_map_simple_offset.py new file mode 100644 index 0000000..e3ba1b9 --- /dev/null +++ b/tests/test_test_map_simple_offset.py @@ -0,0 +1,81 @@ +"""test test_map_simple_offset.tmx""" +import os +from pathlib import Path + +import pytiled_parser + +TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) +TEST_DATA = TESTS_DIR / "test_data" + + +def test_map_simple(): + """ + TMX with a very simple spritesheet tile set and some properties. + """ + + test_map = pytiled_parser.parse_tile_map(TEST_DATA / "test_map_simple_offset.tmx") + + # map + # unsure how to get paths to compare propperly + assert test_map.parent_dir == TEST_DATA + assert test_map.version == "1.2" + assert test_map.tiled_version == "1.3.1" + assert test_map.orientation == "orthogonal" + assert test_map.render_order == "right-down" + assert test_map.map_size == (8, 6) + assert test_map.tile_size == (32, 32) + assert test_map.infinite == False + assert test_map.next_layer_id == 2 + assert test_map.next_object_id == 1 + + # optional, not for orthogonal maps + assert test_map.hex_side_length == None + assert test_map.stagger_axis == None + assert test_map.stagger_index == None + assert test_map.background_color == None + + assert test_map.properties == { + "bool property - false": False, + "bool property - true": True, + "color property": "#ff49fcff", + "float property": 1.23456789, + "int property": 13, + "string property": "Hello, World!!", + } + + # tileset + assert test_map.tile_sets[1].name == "tile_set_image" + assert test_map.tile_sets[1].max_tile_size == (32, 32) + assert test_map.tile_sets[1].spacing == 1 + assert test_map.tile_sets[1].margin == 1 + assert test_map.tile_sets[1].tile_count == 48 + assert test_map.tile_sets[1].columns == 8 + assert test_map.tile_sets[1].tile_offset == None + assert test_map.tile_sets[1].grid == None + assert test_map.tile_sets[1].properties == None + + # unsure how to get paths to compare propperly + assert str(test_map.tile_sets[1].image.source) == ("images/tmw_desert_spacing.png") + assert test_map.tile_sets[1].image.trans == None + assert test_map.tile_sets[1].image.size == (265, 199) + + assert test_map.tile_sets[1].terrain_types == None + assert test_map.tile_sets[1].tiles == {} + + # layers + assert test_map.layers[0].layer_data == [ + [1, 2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31, 32], + [33, 34, 35, 36, 37, 38, 39, 40], + [41, 42, 43, 44, 45, 46, 47, 48], + ] + assert test_map.layers[0].id_ == 1 + assert test_map.layers[0].name == "Tile Layer 1" + assert test_map.layers[0].offset == pytiled_parser.objects.OrderedPair( + x=16.0, y=-16.42 + ) + assert test_map.layers[0].opacity == None + assert test_map.layers[0].properties == None + assert test_map.layers[0].size == (8, 6) diff --git a/tests/test_typing_helpers.py b/tests/test_typing_helpers.py new file mode 100644 index 0000000..e9b9183 --- /dev/null +++ b/tests/test_typing_helpers.py @@ -0,0 +1,19 @@ +"""Tests for typing helpers""" + +import pytest + +from pytiled_parser import typing_helpers as TH + +TEST_IS_FLOAT_PARAMS = [ + (1, True), + ("1", True), + (1.1, True), + ("1.1", True), + ("one", False), + (None, False), +] + + +@pytest.mark.parametrize("string,expected", TEST_IS_FLOAT_PARAMS) +def test_is_float(string, expected): + assert TH.is_float(string) == expected