#!/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())