#!/usr/bin/env python3 """Generate map images from map files""" import argparse import pathlib import os from typing import Sequence, Tuple import yaml import cv2 # type: ignore import numpy MAP_ROOT = pathlib.Path(os.getcwd()) TILES_DIR = MAP_ROOT / '../assets/tiles' SHOW_MAP = False Row = Sequence[str] Tiles = Sequence[Row] FORTIFICATIONS = ['tower', 'gate', 'wall'] def get_tile_path(tile: str) -> pathlib.Path: """Return the corresponding image file name for a tile""" return TILES_DIR / f'{tile.lower().replace(" ", "_")}.png' def is_right_edge(tiles: Tiles, x: int, y: int) -> bool: """Return true if (x,y) is on the right edge of the map""" return len(tiles[y]) - 1 == x def is_left_edge(x: int) -> bool: """Return true if (x,y) is on the left edge of the map""" return x == 0 def is_kind(tile: str, kind) -> bool: """Return if the tile is one of kind""" if kind is None: return tile != 'neutral' if not hasattr(kind, '__iter__'): kind = [kind] return tile in kind def is_connected_to(tiles: Tiles, x: int, y: int, kind=None) -> Tuple[bool, bool, bool, bool]: """Return a Quadruple of bools if the naeighbours of (x,y) are one of kind""" row = tiles[y] left = x > 0 and is_kind(row[x - 1], kind) right = x < len(row) - 1 and is_kind(row[x + 1], kind) above = y > 0 and is_kind(tiles[y - 1][x], kind) below = y < len(tiles) - 1 and is_kind(tiles[y + 1][x], kind) return left, right, above, below def count_connections(left: bool, right: bool, above: bool, below: bool) -> int: """Count the true values in the four connections""" return len([b for b in [left, right, above, below] if b is True]) def find_street_tile(tiles: Tiles, x: int, y: int): # find street continuations left, right, above, below = is_connected_to(tiles, x, y, kind='street') connections = count_connections(left, right, above, below) if connections == 0: # This street is not connected to another street -> # check any other non neutral tiles left, right, above, below = is_connected_to(tiles, x, y, kind=None) connections = count_connections(left, right, above, below) # This street is not connected to anything. Seams odd! # Use a 4 way street if connections == 0: tile_img = cv2.imread(str(get_tile_path('street_4'))) # straight lines elif connections == 1 or (connections == 2 and ((left and right) or (above and below))): tile_img = cv2.imread(str(get_tile_path('street'))) if above or below: tile_img = numpy.rot90(tile_img) # elbow elif connections == 2: # normal orientation above and right tile_img = cv2.imread(str(get_tile_path('street_2'))) if right and below: tile_img = numpy.rot90(tile_img, 3) elif left and below: tile_img = numpy.rot90(tile_img, 2) elif above and left: tile_img = numpy.rot90(tile_img) elif connections == 3: # normal orientation left above right tile_img = cv2.imread(str(get_tile_path('street_3'))) if above and right and below: tile_img = numpy.rot90(tile_img, 3) elif left and below and right: tile_img = numpy.rot90(tile_img, 2) elif below and left and above: tile_img = numpy.rot90(tile_img) elif connections == 4: tile_img = cv2.imread(str(get_tile_path('street_4'))) return tile_img def find_wall_tile(tiles: Sequence[Sequence[str]], x: int, y: int): # find fortification continuations left, right, above, below = is_connected_to(tiles, x, y, kind=FORTIFICATIONS) connections = count_connections(left, right, above, below) if connections == 0: # This wall is not connected to another fortification -> # check any other non neutral tiles left, right, above, below = is_connected_to(tiles, x, y, kind=None) connections = count_connections(left, right, above, below) # This wall is not connected to anything. Seams odd! # Use default horizontal wall if connections == 0: tile_img = cv2.imread(str(get_tile_path('wall'))) # straight walls elif connections == 1 or (connections == 2 and ((left and right) or (above and below))): if above or below: tile_img = cv2.imread(str(get_tile_path('wall_ud'))) else: tile_img = cv2.imread(str(get_tile_path('wall'))) # elbow elif connections == 2: if above: tile_img = cv2.imread(str(get_tile_path('wall_elbow_up'))) else: tile_img = cv2.imread(str(get_tile_path('wall_elbow_down'))) if left: tile_img = cv2.flip(tile_img, 1) elif connections == 3 or connections == 4: # We don't have wall tiles for those structures yet use default vertical wall tile_img = cv2.imread(str(get_tile_path('wall'))) return tile_img def find_gate_tile(tiles: Sequence[Sequence[str]], x: int, y: int): # find relevant structure tiles left, right, above, below = is_connected_to(tiles, x, y, kind=FORTIFICATIONS) connections = count_connections(left, right, above, below) if connections == 0: return cv2.imread(str(get_tile_path('gate'))) # straight gates are the only special gates we have if above or below: return cv2.imread(str(get_tile_path('gate_ud'))) if left or right: return cv2.imread(str(get_tile_path('gate_lr'))) return cv2.imread(str(get_tile_path('gate'))) def find_tower_tile(tiles: Sequence[Sequence[str]], x: int, y: int): # find wall continuations left, right, above, below = is_connected_to(tiles, x, y, kind=FORTIFICATIONS) connections = count_connections(left, right, above, below) # This tower is not connected to a relevant structure if connections == 0: return cv2.imread(str(get_tile_path('tower'))) # generate connection selector selector = '' if left: selector += 'l' if right: selector += 'r' if above: selector += 'u' # corner cases where tower is placed on the edge if selector and 'r' not in selector and is_right_edge(tiles, x, y): selector += 'r' if selector and 'l' not in selector and is_left_edge(x): selector += 'l' tile_name = 'tower' if selector: tile_name = f'{tile_name}_{selector}' return cv2.imread(str(get_tile_path(tile_name))) TILE_SELECTORS = { 'street': find_street_tile, 'gate': find_gate_tile, 'tower': find_tower_tile, 'wall': find_wall_tile, } def generate_img(map_path: pathlib.Path): """Generate a image from a map file""" with open(map_path, 'r', encoding="utf8") as map_file: map_definition = yaml.full_load(map_file) map_string = map_definition['map'] # ensure that all keys are actually strings symbols = {str(k): v for k, v in map_definition['symbols'].items()} # build up tiles tiles = [] for line in map_string.splitlines(): row = [] for symbol in line: tile = "neutral" if symbol != ' ': if symbol not in symbols: print(f'WARNING unknown symbol "{symbol}"') else: tile = symbols[symbol] row.append(tile) tiles.append(row) rows_imgs = [] for y, row in enumerate(tiles): tile_imgs = [] for x, tile in enumerate(row): # find the right street and rotation if tile in TILE_SELECTORS: tile_img = TILE_SELECTORS[tile](tiles, x, y) else: tile_img = cv2.imread(str(get_tile_path(tile))) tile_imgs.append(tile_img) row_img = cv2.hconcat(tile_imgs) rows_imgs.append(row_img) map_img = cv2.vconcat(rows_imgs) if SHOW_MAP: cv2.imshow(f'{map_path.stem}.png', map_img) cv2.waitKey(0) cv2.imwrite(f'{map_path.stem}.png', map_img) def main(map_input): map_path = pathlib.Path(map_input) map_files = [map_path] if map_path.is_dir(): map_files = map_path.glob('*.yml') for m in map_files: generate_img(m) if __name__ == '__main__': parser = argparse.ArgumentParser( description='generate latex standalone cards') parser.add_argument('map_input', help='map file or directory containing map files') parser.add_argument('--map-root', help='path to the map root directory') parser.add_argument('--show-map', help='show the generated map', action='store_true') args = parser.parse_args() if args.map_root: MAP_ROOT = pathlib.Path(args.latex_root) TILES_DIR = MAP_ROOT / '../assets/tiles' if args.show_map: SHOW_MAP = True main(args.map_input)