Skip to content

View

patiencepilot.view

Player-known views of authoritative game state.

PlayerStackCard dataclass

A tableau card as known to a player.

Source code in src/patiencepilot/view.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@dataclass(frozen=True, slots=True)
class PlayerStackCard:
    """A tableau card as known to a player."""

    card: Card | None
    face_up: bool

    def __post_init__(self) -> None:
        """Validate player-view visibility invariants."""
        if self.face_up and self.card is None:
            msg = "face-up player-view cards must expose a card"
            raise InvalidStateError(msg)
        if not self.face_up and self.card is not None:
            msg = "face-down player-view cards must not expose a card"
            raise InvalidStateError(msg)

    @classmethod
    def visible(cls, card: Card) -> PlayerStackCard:
        """Return a face-up player-visible tableau card."""
        return cls(card=card, face_up=True)

    @classmethod
    def hidden(cls) -> PlayerStackCard:
        """Return a face-down player-hidden tableau card."""
        return cls(card=None, face_up=False)

    @property
    def visible_card(self) -> Card | None:
        """Return the card if it is visible to the player."""
        return self.card if self.face_up else None

visible_card property

visible_card: Card | None

Return the card if it is visible to the player.

__post_init__

__post_init__() -> None

Validate player-view visibility invariants.

Source code in src/patiencepilot/view.py
20
21
22
23
24
25
26
27
def __post_init__(self) -> None:
    """Validate player-view visibility invariants."""
    if self.face_up and self.card is None:
        msg = "face-up player-view cards must expose a card"
        raise InvalidStateError(msg)
    if not self.face_up and self.card is not None:
        msg = "face-down player-view cards must not expose a card"
        raise InvalidStateError(msg)

visible classmethod

visible(card: Card) -> PlayerStackCard

Return a face-up player-visible tableau card.

Source code in src/patiencepilot/view.py
29
30
31
32
@classmethod
def visible(cls, card: Card) -> PlayerStackCard:
    """Return a face-up player-visible tableau card."""
    return cls(card=card, face_up=True)

hidden classmethod

hidden() -> PlayerStackCard

Return a face-down player-hidden tableau card.

Source code in src/patiencepilot/view.py
34
35
36
37
@classmethod
def hidden(cls) -> PlayerStackCard:
    """Return a face-down player-hidden tableau card."""
    return cls(card=None, face_up=False)

UnknownCardConstraints dataclass

Constraints for cards not currently identified in the player view.

Source code in src/patiencepilot/view.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@dataclass(frozen=True, slots=True)
class UnknownCardConstraints:
    """Constraints for cards not currently identified in the player view."""

    hidden_tableau_counts: tuple[int, ...]
    stock_count: int
    unseen_cards: tuple[Card, ...]

    def __post_init__(self) -> None:
        """Validate unknown-card constraint counts."""
        if len(self.hidden_tableau_counts) != N_TABLEAU_COLUMNS:
            msg = f"hidden_tableau_counts must contain {N_TABLEAU_COLUMNS} entries"
            raise InvalidStateError(msg)
        if any(count < 0 for count in self.hidden_tableau_counts):
            msg = "hidden tableau counts must be non-negative"
            raise InvalidStateError(msg)
        if self.stock_count < 0:
            msg = "stock_count must be non-negative"
            raise InvalidStateError(msg)

    @property
    def hidden_tableau_count(self) -> int:
        """Return the total number of face-down tableau cards."""
        return sum(self.hidden_tableau_counts)

    @property
    def hidden_position_count(self) -> int:
        """Return the number of hidden positions in stock and tableau."""
        return self.stock_count + self.hidden_tableau_count

hidden_tableau_count property

hidden_tableau_count: int

Return the total number of face-down tableau cards.

hidden_position_count property

hidden_position_count: int

Return the number of hidden positions in stock and tableau.

__post_init__

__post_init__() -> None

Validate unknown-card constraint counts.

Source code in src/patiencepilot/view.py
53
54
55
56
57
58
59
60
61
62
63
def __post_init__(self) -> None:
    """Validate unknown-card constraint counts."""
    if len(self.hidden_tableau_counts) != N_TABLEAU_COLUMNS:
        msg = f"hidden_tableau_counts must contain {N_TABLEAU_COLUMNS} entries"
        raise InvalidStateError(msg)
    if any(count < 0 for count in self.hidden_tableau_counts):
        msg = "hidden tableau counts must be non-negative"
        raise InvalidStateError(msg)
    if self.stock_count < 0:
        msg = "stock_count must be non-negative"
        raise InvalidStateError(msg)

PlayerView dataclass

A state projection containing only what a perfect human player may know.

Source code in src/patiencepilot/view.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
@dataclass(frozen=True, slots=True)
class PlayerView:
    """A state projection containing only what a perfect human player may know."""

    foundations: tuple[tuple[Card, ...], ...]
    tableau: tuple[tuple[PlayerStackCard, ...], ...]
    waste: tuple[Card, ...]
    seen_cards: tuple[Card, ...]
    unknown: UnknownCardConstraints
    variant: str = "klondike"
    draw_count: int = 1
    redeals_allowed: int | None = None
    redeals_used: int = 0

    @classmethod
    def from_state(cls, state: GameState, *, seen_cards: Iterable[Card] = ()) -> PlayerView:
        """Project an authoritative game state into a player-known view.

        Args:
            state: Authoritative state to project.
            seen_cards: Cards observed earlier in the game, even if they are no
                longer visible.
        """
        tableau = tuple(
            tuple(
                PlayerStackCard.visible(stack_card.card) if stack_card.face_up else PlayerStackCard.hidden()
                for stack_card in column
            )
            for column in state.tableau
        )
        visible_cards = tuple(cls._iter_visible_cards(state))
        known_cards = _standard_deck_ordered_unique((*seen_cards, *visible_cards))
        known_set = set(known_cards)
        unknown = UnknownCardConstraints(
            hidden_tableau_counts=tuple(
                sum(1 for stack_card in column if not stack_card.face_up) for column in state.tableau
            ),
            stock_count=len(state.stock),
            unseen_cards=tuple(card for card in standard_deck() if card not in known_set),
        )
        return cls(
            foundations=state.foundations,
            tableau=tableau,
            waste=state.waste,
            seen_cards=known_cards,
            unknown=unknown,
            variant=state.variant,
            draw_count=state.draw_count,
            redeals_allowed=state.redeals_allowed,
            redeals_used=state.redeals_used,
        )

    def foundation(self, suit: Suit) -> tuple[Card, ...]:
        """Return the visible foundation stack for ``suit``."""
        return self.foundations[SUIT_ORDER.index(suit)]

    @property
    def stock_count(self) -> int:
        """Return the number of cards hidden in stock."""
        return self.unknown.stock_count

    def iter_visible_cards(self) -> Iterator[Card]:
        """Yield cards currently visible to the player."""
        yield from self._iter_visible_cards(self)

    @staticmethod
    def _iter_visible_cards(state: GameState | PlayerView) -> Iterator[Card]:
        """Yield visible cards from an authoritative state or player view."""
        for foundation in state.foundations:
            yield from foundation
        for column in state.tableau:
            for stack_card in column:
                if stack_card.visible_card is not None:
                    yield stack_card.visible_card
        yield from state.waste

stock_count property

stock_count: int

Return the number of cards hidden in stock.

from_state classmethod

from_state(
    state: GameState, *, seen_cards: Iterable[Card] = ()
) -> PlayerView

Project an authoritative game state into a player-known view.

Parameters:

Name Type Description Default
state GameState

Authoritative state to project.

required
seen_cards Iterable[Card]

Cards observed earlier in the game, even if they are no longer visible.

()
Source code in src/patiencepilot/view.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
@classmethod
def from_state(cls, state: GameState, *, seen_cards: Iterable[Card] = ()) -> PlayerView:
    """Project an authoritative game state into a player-known view.

    Args:
        state: Authoritative state to project.
        seen_cards: Cards observed earlier in the game, even if they are no
            longer visible.
    """
    tableau = tuple(
        tuple(
            PlayerStackCard.visible(stack_card.card) if stack_card.face_up else PlayerStackCard.hidden()
            for stack_card in column
        )
        for column in state.tableau
    )
    visible_cards = tuple(cls._iter_visible_cards(state))
    known_cards = _standard_deck_ordered_unique((*seen_cards, *visible_cards))
    known_set = set(known_cards)
    unknown = UnknownCardConstraints(
        hidden_tableau_counts=tuple(
            sum(1 for stack_card in column if not stack_card.face_up) for column in state.tableau
        ),
        stock_count=len(state.stock),
        unseen_cards=tuple(card for card in standard_deck() if card not in known_set),
    )
    return cls(
        foundations=state.foundations,
        tableau=tableau,
        waste=state.waste,
        seen_cards=known_cards,
        unknown=unknown,
        variant=state.variant,
        draw_count=state.draw_count,
        redeals_allowed=state.redeals_allowed,
        redeals_used=state.redeals_used,
    )

foundation

foundation(suit: Suit) -> tuple[Card, ...]

Return the visible foundation stack for suit.

Source code in src/patiencepilot/view.py
128
129
130
def foundation(self, suit: Suit) -> tuple[Card, ...]:
    """Return the visible foundation stack for ``suit``."""
    return self.foundations[SUIT_ORDER.index(suit)]

iter_visible_cards

iter_visible_cards() -> Iterator[Card]

Yield cards currently visible to the player.

Source code in src/patiencepilot/view.py
137
138
139
def iter_visible_cards(self) -> Iterator[Card]:
    """Yield cards currently visible to the player."""
    yield from self._iter_visible_cards(self)