Source code for yasuki_core.engine.snapshot

from dataclasses import dataclass, field, replace

from yasuki_core.engine.players import PlayerId
from yasuki_core.engine.zones import ProvinceZone
from yasuki_core.engine.table import TableState, SeatInfo, ZoneKey, DeckKey, BoardPos
from yasuki_core.game_pieces.cards import L5RCard
from yasuki_core.engine.serialization import (
    encode_card,
    decode_card,
    encode_zone_key,
    decode_zone_key,
    encode_deck_key,
    decode_deck_key,
    encode_seat,
    decode_seat,
    encode_attach_target,
    decode_attach_target,
)

# The start-of-game table snapshot: a deep-copied capture of a dealt table, the rebuild that turns
# it back into a live TableState, and its JSON codec. Product-neutral foundation — the manual sim's
# action log and the rules engine's game log both seed a replay from it.


[docs] @dataclass(slots=True) class InitialRecord: """A complete table snapshot that seeds a replay. Captures the full state at the log head — seats, every owned zone and deck with its ordered contents, and the battlefield with positions — so a replay rebuilds the table exactly and then folds the recorded tape onto it. Attributes ---------- seats : dict mapping PlayerId to SeatInfo Each seat's status (name, honor, ready, connected). decklists : dict mapping DeckKey to list of L5RCard The ordered contents of each fate and dynasty deck. zones : dict mapping ZoneKey to list of L5RCard The contents of every owned zone, including provinces. battlefield : list of L5RCard The shared battlefield's cards. positions : dict mapping str to BoardPos Battlefield card positions, keyed by card id. attachments : dict mapping str to (str or ZoneKey) The attachment graph, keyed by attached card id, mapping to a parent card id or province. creatable_tokens : dict mapping str to L5RCard Token templates the loaded decks can create, keyed by token card id, so a replayed token spawn resolves against the same templates without a database call. setup_seeds : dict mapping str to int Named RNG seeds used during setup that no logged entry carries. """ seats: dict[PlayerId, SeatInfo] decklists: dict[DeckKey, list[L5RCard]] zones: dict[ZoneKey, list[L5RCard]] = field(default_factory=dict) battlefield: list[L5RCard] = field(default_factory=list) positions: dict[str, BoardPos] = field(default_factory=dict) attachments: dict[str, str | ZoneKey] = field(default_factory=dict) creatable_tokens: dict[str, L5RCard] = field(default_factory=dict) setup_seeds: dict[str, int] = field(default_factory=dict)
[docs] @classmethod def from_state( cls, state: TableState, setup_seeds: dict[str, int] | None = None ) -> "InitialRecord": """Snapshot ``state`` into an initial record, deep-copying every card so later in-place mutation of the live table never touches the snapshot. Parameters ---------- state : TableState The table to capture. setup_seeds : dict mapping str to int, optional Named setup seeds to record. Default empty. """ return cls( seats={pid: replace(info) for pid, info in state.seats.items()}, decklists={ key: [replace(card) for card in deck.cards] for key, deck in state.decks.items() }, zones={ key: [replace(card) for card in zone.cards] for key, zone in state.zones.items() }, battlefield=[replace(card) for card in state.battlefield.cards], positions=dict(state.positions), attachments=dict(state.attachments), creatable_tokens={tid: replace(card) for tid, card in state.creatable_tokens.items()}, setup_seeds=dict(setup_seeds or {}), )
[docs] def build_initial_state(initial: InitialRecord) -> TableState: """Rebuild a full ``TableState`` from an initial record: the recorded seats, decks, zones, battlefield, and positions, with every card deep-copied so the record stays pristine and repeated builds are independent.""" state = TableState.empty_two_seat() for pid, info in initial.seats.items(): state.seats[pid] = replace(info) for key, cards in initial.decklists.items(): state.decks[key].cards = _restore_cards(state, cards) for key, cards in initial.zones.items(): zone = state.zones.get(key) if zone is None: zone = ProvinceZone(owner=key.owner) # provinces are the only on-demand zone state.zones[key] = zone zone.cards = _restore_cards(state, cards) state.battlefield.cards = _restore_cards(state, initial.battlefield) state.positions = dict(initial.positions) state.attachments = dict(initial.attachments) state.creatable_tokens = {tid: replace(card) for tid, card in initial.creatable_tokens.items()} return state
def _restore_cards(state: TableState, cards: list[L5RCard]) -> list[L5RCard]: copied = [replace(card) for card in cards] for card in copied: state.cards_by_id[card.id] = card return copied
[docs] def encode_initial(initial: InitialRecord) -> dict: """Encode an ``InitialRecord`` to JSON-ready plain data.""" return { "seats": [ {"seat": pid.name, "info": encode_seat(info)} for pid, info in initial.seats.items() ], "decklists": [ {"deck": encode_deck_key(key), "cards": [encode_card(card) for card in cards]} for key, cards in initial.decklists.items() ], "zones": [ {"zone": encode_zone_key(key), "cards": [encode_card(card) for card in cards]} for key, cards in initial.zones.items() ], "battlefield": [encode_card(card) for card in initial.battlefield], "positions": {card_id: [pos.x, pos.y] for card_id, pos in initial.positions.items()}, "attachments": { card_id: encode_attach_target(target) for card_id, target in initial.attachments.items() }, "creatable_tokens": { tid: encode_card(card) for tid, card in initial.creatable_tokens.items() }, "setup_seeds": dict(initial.setup_seeds), }
[docs] def decode_initial(payload: dict) -> InitialRecord: """Rebuild the ``InitialRecord`` encoded by ``encode_initial``.""" seats = {PlayerId[item["seat"]]: decode_seat(item["info"]) for item in payload["seats"]} decklists = { decode_deck_key(item["deck"]): [decode_card(card) for card in item["cards"]] for item in payload["decklists"] } zones = { decode_zone_key(item["zone"]): [decode_card(card) for card in item["cards"]] for item in payload["zones"] } battlefield = [decode_card(card) for card in payload["battlefield"]] positions = {card_id: BoardPos(*xy) for card_id, xy in payload["positions"].items()} attachments = { card_id: decode_attach_target(target) for card_id, target in payload.get("attachments", {}).items() } creatable_tokens = { tid: decode_card(card) for tid, card in payload.get("creatable_tokens", {}).items() } return InitialRecord( seats=seats, decklists=decklists, zones=zones, battlefield=battlefield, positions=positions, attachments=attachments, creatable_tokens=creatable_tokens, setup_seeds=dict(payload["setup_seeds"]), )