Source code for yasuki_core.engine.redaction

from dataclasses import dataclass, field
from typing import Final

from yasuki_core.engine.players import PlayerId
from yasuki_core.engine.table import TableState, ZoneKey, ZoneRole, DeckKey, BoardPos
from yasuki_core.game_pieces.cards import L5RCard
from yasuki_core.game_pieces.constants import Side


[docs] @dataclass(frozen=True, slots=True) class HiddenCard: """A card the viewer may not identify, carrying only a stable id, its side (which back art to draw), its owner (whose card it is — public; only the face is secret), a constant ``face`` marker, and ``shown`` — the owner's own disclosure marker, kept so their shown face-down card keeps its reveal cue. The real name, text, and front image never reach this viewer's snapshot.""" card_id: str side: Side owner: PlayerId | None = None face: str = "back" shown: bool = False
CardView = L5RCard | HiddenCard
[docs] @dataclass(frozen=True, slots=True) class SeatView: name: str honor: int ready: bool connected: bool avatar: dict | None = None
[docs] @dataclass(frozen=True, slots=True) class ZoneView: cards: tuple[CardView, ...]
[docs] @dataclass(frozen=True, slots=True) class DeckView: count: int top: L5RCard | None # the real top card only when it has been flipped face up
[docs] @dataclass(frozen=True, slots=True) class BattlefieldCardView: card: CardView pos: BoardPos
[docs] @dataclass(frozen=True, slots=True) class ViewSnapshot: seq: int viewer: PlayerId seats: dict[PlayerId, SeatView] zones: dict[ZoneKey, ZoneView] decks: dict[DeckKey, DeckView] battlefield: tuple[BattlefieldCardView, ...] # Ids of cards this viewer sees solely because they are peeking them — visible to the viewer alone, # so the client renders them with the reduced-opacity peek cue. Empty when nothing is being peeked. peeked_ids: frozenset[str] = frozenset() # The attachment graph, keyed by attached card id; each value is a parent card id or a province # ``ZoneKey``. Passed through verbatim — card ids are public, so an attachment referencing a card # the viewer cannot identify still resolves by id against the (hidden) card in the same snapshot. attachments: dict[str, "str | ZoneKey"] = field(default_factory=dict)
# Zones whose contents are public to both seats. _PUBLIC_ROLES: Final = frozenset( { ZoneRole.FATE_DISCARD, ZoneRole.FATE_BANISH, ZoneRole.DYNASTY_DISCARD, ZoneRole.DYNASTY_BANISH, } ) def _hide(card: L5RCard) -> HiddenCard: # Only an owned card's show is a real disclosure (a public card's show reveals to nobody), and the # owner alone ever sees a stub of their own shown card — so gate the marker on ownership. return HiddenCard( card_id=card.id, side=card.side, owner=card.owner, shown=card.shown and card.owner is not None, ) def _opponent(seat: PlayerId) -> PlayerId: """The other seat at a two-seat table.""" return PlayerId.P2 if seat is PlayerId.P1 else PlayerId.P1 def _shown_to(card: L5RCard, viewer: PlayerId) -> bool: """Whether ``card`` is shown to ``viewer`` as the owner's opponent — the disclosure a face-down card's owner makes to the other seat without turning the card face up.""" return card.shown and card.owner is not None and viewer == _opponent(card.owner) def _default_visible(card: L5RCard, viewer: PlayerId, role: ZoneRole | None) -> bool: """The baseline visibility before any show/peek disclosure: public discard/banish to all, the hand to its owner, and a face-up card on the battlefield (``role`` None) or in a province.""" if role in _PUBLIC_ROLES: return True if role is ZoneRole.HAND: return card.owner == viewer return card.face_up def _zone_card_visible(card: L5RCard, viewer: PlayerId, role: ZoneRole | None) -> bool: """Whether ``viewer`` may identify ``card`` sitting in ``role`` (None for the battlefield): by the baseline rule, because the owner shows it to this opponent, or because this viewer is peeking it.""" return _default_visible(card, viewer, role) or _shown_to(card, viewer) or viewer in card.peekers def _peeked_only(card: L5RCard, viewer: PlayerId, role: ZoneRole | None) -> bool: """Whether ``viewer`` sees ``card`` solely through their own peek — visible, but neither by the baseline rule nor through a show. These are the ids the snapshot flags for the peek cue.""" return ( viewer in card.peekers and not _default_visible(card, viewer, role) and not _shown_to(card, viewer) )
[docs] def card_identity_public(state: TableState, card_id: str) -> bool: """Return whether every seat may currently identify this card, given where it sits, its face, and any show. True for a battlefield or province card that is face up, for any card in a public discard or banish, and for a hand card its owner has shown (now public to all); False for a card in a deck, one lying face down to its owner, or one only an opponent or a peeker can see.""" card = state.cards_by_id.get(card_id) if card is None: return False if any(held is card for held in state.battlefield.cards): return all(_zone_card_visible(card, seat, None) for seat in state.seats) for key, zone in state.zones.items(): if any(held is card for held in zone.cards): return all(_zone_card_visible(card, seat, key.role) for seat in state.seats) return False # in a deck or otherwise unlocated — hidden
def _project(card: L5RCard, visible: bool) -> CardView: return card if visible else _hide(card)
[docs] def redact(state: TableState, viewer: PlayerId) -> ViewSnapshot: """Project the authoritative table into the per-viewer view, replacing every card the viewer is not entitled to identify with a :class:`HiddenCard` stub. Visibility: - hand: the owner sees it; others see a back unless the owner has ``shown`` it (then public). - battlefield and provinces: a card is shown only when ``face_up`` — a face-down card is a back to everyone, its owner included, unless the owner has ``shown`` it to the other seat. - discards and banishes: public to both seats. - decks: count only, plus the top card when it has been flipped ``face_up``. A peeker sees any card it is peeking, whoever owns it; the returned snapshot records those ids in ``peeked_ids`` so the client marks them as a private peek. Card ids survive redaction so a client can animate a card it cannot yet identify (an opponent's draw is a back sliding from deck to hand). Parameters ---------- state : TableState The authoritative table. viewer : PlayerId The seat the snapshot is built for. """ seats = { seat: SeatView(info.name, info.honor, info.ready, info.connected, info.avatar) for seat, info in state.seats.items() } peeked_ids: set[str] = set() zones = {} for key, zone in state.zones.items(): views = [] for card in zone.cards: views.append(_project(card, _zone_card_visible(card, viewer, key.role))) if _peeked_only(card, viewer, key.role): peeked_ids.add(card.id) zones[key] = ZoneView(tuple(views)) decks = {} for key, deck in state.decks.items(): top = deck.cards[-1] if deck.cards else None shown_top = top if top is not None and top.face_up else None decks[key] = DeckView(count=len(deck.cards), top=shown_top) battlefield_views = [] for card in state.battlefield.cards: battlefield_views.append( BattlefieldCardView( _project(card, _zone_card_visible(card, viewer, None)), state.positions.get(card.id, BoardPos(0.0, 0.0)), ) ) if _peeked_only(card, viewer, None): peeked_ids.add(card.id) return ViewSnapshot( seq=state.seq, viewer=viewer, seats=seats, zones=zones, decks=decks, battlefield=tuple(battlefield_views), peeked_ids=frozenset(peeked_ids), attachments=dict(state.attachments), )