|
| 1 | +"""Project a legal plane view into an AHBG presentation snapshot. |
| 2 | +
|
| 3 | +This is graphics. It does not decide adjacency, legality, or turn resolution. |
| 4 | +Unknown observation fields are ignored. Seed, RNG, and event-log internals |
| 5 | +are not copied into the snapshot. |
| 6 | +""" |
| 7 | + |
| 8 | +from __future__ import annotations |
| 9 | + |
| 10 | +from typing import Any, Mapping, Sequence |
| 11 | + |
| 12 | +from snapshot import KIND, STANDING, PresentationSnapshotError, validate_snapshot |
| 13 | + |
| 14 | + |
| 15 | +def snapshot_from_observation( |
| 16 | + observation: Mapping[str, Any], |
| 17 | + *, |
| 18 | + plane_id: str, |
| 19 | + selected_tile: str | None = None, |
| 20 | + feed: Sequence[Mapping[str, Any]] = (), |
| 21 | + move_events: Sequence[Mapping[str, Any]] = (), |
| 22 | +) -> dict[str, Any]: |
| 23 | + """Map a public observation (and optional resolved move events) to a snapshot. |
| 24 | +
|
| 25 | + ``observation`` is the legal view: ``turn``, ``tiles``, ``units``. A full |
| 26 | + plane dict is also accepted; ``seed`` and ``schema`` are dropped. |
| 27 | + ``move_events`` are already-resolved ``move`` payloads with ``unit_id``, |
| 28 | + ``from_tile_id``, and ``to_tile_id``. |
| 29 | + """ |
| 30 | + |
| 31 | + if not isinstance(observation, Mapping): |
| 32 | + raise PresentationSnapshotError("observation must be an object") |
| 33 | + if not isinstance(plane_id, str) or not plane_id: |
| 34 | + raise PresentationSnapshotError("plane_id must be exact non-empty text") |
| 35 | + turn = observation.get("turn") |
| 36 | + raw_tiles = observation.get("tiles") |
| 37 | + raw_units = observation.get("units") |
| 38 | + if not isinstance(raw_tiles, list) or not raw_tiles: |
| 39 | + raise PresentationSnapshotError("observation tiles must be a non-empty list") |
| 40 | + if not isinstance(raw_units, list): |
| 41 | + raise PresentationSnapshotError("observation units must be a list") |
| 42 | + |
| 43 | + tiles: list[dict[str, Any]] = [] |
| 44 | + for tile in raw_tiles: |
| 45 | + if not isinstance(tile, Mapping): |
| 46 | + raise PresentationSnapshotError("each observation tile must be an object") |
| 47 | + tile_id = tile.get("tile_id", tile.get("id")) |
| 48 | + if not isinstance(tile_id, str) or not tile_id: |
| 49 | + raise PresentationSnapshotError("observation tile id must be exact non-empty text") |
| 50 | + presented: dict[str, Any] = {"id": tile_id, "q": tile.get("q"), "r": tile.get("r")} |
| 51 | + label = tile.get("label") |
| 52 | + if isinstance(label, str) and label: |
| 53 | + presented["label"] = label |
| 54 | + tiles.append(presented) |
| 55 | + |
| 56 | + units: list[dict[str, Any]] = [] |
| 57 | + for unit in raw_units: |
| 58 | + if not isinstance(unit, Mapping): |
| 59 | + raise PresentationSnapshotError("each observation unit must be an object") |
| 60 | + unit_id = unit.get("unit_id", unit.get("id")) |
| 61 | + tile_id = unit.get("tile_id", unit.get("tile")) |
| 62 | + if not isinstance(unit_id, str) or not unit_id: |
| 63 | + raise PresentationSnapshotError("observation unit id must be exact non-empty text") |
| 64 | + presented_unit: dict[str, Any] = {"id": unit_id, "tile": tile_id} |
| 65 | + label = unit.get("label") |
| 66 | + if isinstance(label, str) and label: |
| 67 | + presented_unit["label"] = label |
| 68 | + units.append(presented_unit) |
| 69 | + |
| 70 | + motions: list[dict[str, str]] = [] |
| 71 | + for event in move_events: |
| 72 | + if not isinstance(event, Mapping): |
| 73 | + raise PresentationSnapshotError("each move event must be an object") |
| 74 | + kind = event.get("kind") |
| 75 | + data = event.get("data", event) |
| 76 | + if kind not in (None, "move"): |
| 77 | + continue |
| 78 | + if not isinstance(data, Mapping): |
| 79 | + raise PresentationSnapshotError("move event data must be an object") |
| 80 | + motions.append( |
| 81 | + { |
| 82 | + "unit": str(data.get("unit_id", "")), |
| 83 | + "from": str(data.get("from_tile_id", "")), |
| 84 | + "to": str(data.get("to_tile_id", "")), |
| 85 | + } |
| 86 | + ) |
| 87 | + |
| 88 | + if selected_tile is None and units: |
| 89 | + selected_tile = units[0].get("tile") if isinstance(units[0].get("tile"), str) else None |
| 90 | + |
| 91 | + snapshot = { |
| 92 | + "kind": KIND, |
| 93 | + "standing": STANDING, |
| 94 | + "plane_id": plane_id, |
| 95 | + "turn": turn, |
| 96 | + "tiles": tiles, |
| 97 | + "units": units, |
| 98 | + "selected_tile": selected_tile, |
| 99 | + "feed": [dict(item) for item in feed], |
| 100 | + "motions": motions, |
| 101 | + } |
| 102 | + if not motions: |
| 103 | + snapshot.pop("motions") |
| 104 | + return dict(validate_snapshot(snapshot)) |
0 commit comments