#!/usr/bin/env python3 """Read the boards the daily scenarios name, and write the card's tile data. The card draws a small minimap of the real terrain beside the schematic board. That terrain lives in MegaMek's `.board` files, which are in the arena image and not on the website, so it is read here at authoring time and committed — the same arrangement as daily.json. scripts/boards.py --megamek --out web/src/content/boards.json What is encoded is only what the tile draws: one character per hex for terrain class, one for level, and an exit mask per road hex. Everything else in a `.board` file — themes, elevations of individual terrain layers, the fluff — is dropped, because the tile is a summary and so is this. Format, from megamek/common/board/Board.java's own reader: size W H hex "" "" The coordinate on a hex line is ignored by MegaMek itself (Board.java says so in a comment): hexes are consumed in file order, so hex n is at (n % width, n / width). Terrain is `;`-separated `name:level[:exits]`. """ import argparse import json import pathlib import re import sys # What the tile draws, in the order a reader should notice it. The character is # what goes in the payload. OPEN, LIGHT_WOODS, HEAVY_WOODS, WATER, ROAD, BUILDING = ".", "w", "W", "~", "=", "#" LEVELS = "abcdefghijklmnopqrs" # -9..9, so a level is always one character def classify(terrain: str) -> tuple[str, int]: """The one class a hex reads as, and its road exit mask.""" kinds, exits = {}, 0 for part in terrain.split(";"): if not part: continue bits = part.split(":") name = bits[0] level = bits[1] if len(bits) > 1 else "1" kinds[name] = level if name in ("road", "bridge") and len(bits) > 2: try: exits = int(bits[2]) except ValueError: pass # Most-to-least significant for a glance: a building on a road is a # building, a road through woods is a road. if "building" in kinds or "fuel_tank" in kinds: return BUILDING, exits if "road" in kinds or "bridge" in kinds or "pavement" in kinds: return ROAD, exits if "water" in kinds: return WATER, exits if "woods" in kinds: return (HEAVY_WOODS if kinds["woods"] not in ("1",) else LIGHT_WOODS), exits if "jungle" in kinds: return HEAVY_WOODS, exits return OPEN, exits def read_board(path: pathlib.Path) -> dict: width = height = 0 terrain, levels, roads = [], [], {} for line in path.read_text(errors="replace").splitlines(): line = line.strip() if line.startswith("size "): _, w, h = line.split() width, height = int(w), int(h) elif line.startswith("hex "): parts = re.findall(r'"[^"]*"|\S+', line) level = int(parts[2]) kind, exits = classify(parts[3].strip('"')) index = len(terrain) terrain.append(kind) levels.append(LEVELS[max(-9, min(9, level)) + 9]) if kind == ROAD and exits: roads[index] = f"{exits:x}" if not width or len(terrain) != width * height: raise SystemExit(f"{path}: {len(terrain)} hexes for a {width}x{height} board") return { "w": width, "h": height, "t": "".join(terrain), "l": "".join(levels), # Only the road hexes that declare an exit mask; the rest join up by # sitting next to each other, which the renderer can infer. "r": {str(k): v for k, v in roads.items()}, } def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--megamek", required=True) ap.add_argument("--scenarios", required=True) ap.add_argument("--out", required=True) args = ap.parse_args() # Every board any scenario names. wanted = set() for mms in sorted(pathlib.Path(args.scenarios).glob("*.mms")): for m in re.finditer(r"(?:^map:[ \t]+|^\s*- file:[ \t]*)(\S.*)$", mms.read_text(), re.M): name = m.group(1).strip().strip('"') # `map:` followed by a block opens a grid; only a # scalar or a `- file:` names a board. if name and not name.endswith(":") and name.endswith(".board"): wanted.add(name) boards = {} for name in sorted(wanted): path = pathlib.Path(args.megamek) / "data" / "boards" / name if not path.exists(): print(f"boards: {name} is not in {args.megamek}/data/boards", file=sys.stderr) return 1 boards[name] = read_board(path) out = pathlib.Path(args.out) out.write_text(json.dumps({"boards": boards}, indent=1, sort_keys=True) + "\n") total = sum(len(b["t"]) for b in boards.values()) print(f"wrote {out} ({len(boards)} boards, {total} hexes)") return 0 if __name__ == "__main__": raise SystemExit(main())