#!/usr/bin/env python3 """Dependency-free independent validator for Partridge placement JSON.""" from __future__ import annotations import argparse import json import sys from typing import Any def validate(document: dict[str, Any]) -> list[str]: """Return diagnostics for a placement document, or an empty list.""" diagnostics: list[str] = [] try: order = int(document["order"]) board_side = int(document["board_side"]) placements = document["placements"] except (KeyError, TypeError, ValueError) as error: return [f"invalid document: {error}"] expected_side = order * (order + 1) // 2 if order < 1: diagnostics.append("order must be positive") if board_side != expected_side: diagnostics.append( f"board side is {board_side}; expected triangular number {expected_side}" ) if not isinstance(placements, list): return diagnostics + ["placements must be a list"] if board_side < 0: return diagnostics + ["board side must not be negative"] multiplicities = [0] * (max(order, 0) + 1) occupied = [-1] * (board_side * board_side) for index, raw in enumerate(placements): try: x = int(raw["x"]) y = int(raw["y"]) side = int(raw["side"]) except (KeyError, TypeError, ValueError) as error: diagnostics.append(f"placement {index} is malformed: {error}") continue description = f"placement {index} at ({x}, {y}) with side {side}" if side < 1 or side > order: diagnostics.append(f"{description} has an invalid side length") continue multiplicities[side] += 1 if x < 0 or y < 0 or x + side > board_side or y + side > board_side: diagnostics.append(f"{description} is outside the board bounds") continue overlap = -1 for row in range(y, y + side): for column in range(x, x + side): cell = column + row * board_side if occupied[cell] != -1: overlap = occupied[cell] else: occupied[cell] = index if overlap != -1: diagnostics.append(f"{description} overlaps placement {overlap}") for side in range(1, order + 1): if multiplicities[side] != side: diagnostics.append( f"side {side} has multiplicity {multiplicities[side]}; expected {side}" ) if -1 in occupied: diagnostics.append("board is not completely covered") return diagnostics def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "input", nargs="?", type=argparse.FileType("r", encoding="utf-8"), default=sys.stdin, ) args = parser.parse_args() document = json.load(args.input) diagnostics = validate(document) json.dump( {"valid": not diagnostics, "diagnostics": diagnostics}, sys.stdout, indent=2, sort_keys=True, ) sys.stdout.write("\n") return 0 if not diagnostics else 1 if __name__ == "__main__": raise SystemExit(main())