Skip to content

Game

patiencepilot.game

Session wrapper and convenience functions for Solitaire games.

SessionStep dataclass

One move applied within a game session.

Source code in src/patiencepilot/game.py
17
18
19
20
21
22
23
24
25
26
27
28
@dataclass(frozen=True, slots=True)
class SessionStep:
    """One move applied within a game session."""

    move: Move
    before: GameState
    result: MoveResult

    @property
    def after(self) -> GameState:
        """Return the state after the move was applied."""
        return self.result.state

after property

after: GameState

Return the state after the move was applied.

GameSession dataclass

Thin mutable wrapper around pure engine state transitions.

Source code in src/patiencepilot/game.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 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
 74
 75
 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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
@dataclass(slots=True)
class GameSession:
    """Thin mutable wrapper around pure engine state transitions."""

    state: GameState
    variant: Variant | str | None = None
    seed: Seed = None
    metadata: dict[str, object] = field(default_factory=dict)
    ui_state: dict[str, object] = field(default_factory=dict)
    _history: list[SessionStep] = field(default_factory=list, init=False, repr=False)
    _redo_stack: list[SessionStep] = field(default_factory=list, init=False, repr=False)

    def __post_init__(self) -> None:
        """Validate the starting state."""
        engine.validate_state(self.state, self.variant)

    @classmethod
    def new(
        cls,
        variant: Variant | str | None = None,
        *,
        seed: Seed = None,
        options: VariantOptions | None = None,
        metadata: Mapping[str, object] | None = None,
        ui_state: Mapping[str, object] | None = None,
    ) -> GameSession:
        """Return a session around a newly dealt game.

        Args:
            variant: Rule set or registered variant name to use. Defaults to
                Klondike draw-one with unlimited redeals.
            seed: Optional deterministic random seed.
            options: Variant options when ``variant`` is a name or omitted.
            metadata: Optional session metadata for callers or persistence.
            ui_state: Optional UI-only state for adapters.
        """
        state = engine.new_game(variant, seed=seed, options=options)
        return cls(
            state=state,
            variant=variant,
            seed=seed,
            metadata={} if metadata is None else dict(metadata),
            ui_state={} if ui_state is None else dict(ui_state),
        )

    @property
    def history(self) -> tuple[SessionStep, ...]:
        """Return applied session steps in order."""
        return tuple(self._history)

    @property
    def redo_history(self) -> tuple[SessionStep, ...]:
        """Return undone session steps available for redo, oldest first."""
        return tuple(reversed(self._redo_stack))

    @property
    def move_history(self) -> tuple[Move, ...]:
        """Return moves applied in this session."""
        return tuple(step.move for step in self._history)

    @property
    def result_history(self) -> tuple[MoveResult, ...]:
        """Return move results applied in this session."""
        return tuple(step.result for step in self._history)

    @property
    def last_result(self) -> MoveResult | None:
        """Return the most recent move result, if any."""
        if not self._history:
            return None
        return self._history[-1].result

    @property
    def can_undo(self) -> bool:
        """Return whether the session has a move to undo."""
        return bool(self._history)

    @property
    def can_redo(self) -> bool:
        """Return whether the session has a move to redo."""
        return bool(self._redo_stack)

    def legal_moves(self) -> tuple[Move, ...]:
        """Return legal moves from the current state."""
        return engine.legal_moves(self.state, self.variant)

    def validate_state(self) -> None:
        """Validate the current state."""
        engine.validate_state(self.state, self.variant)

    def is_won(self) -> bool:
        """Return whether the current state is won."""
        return engine.is_won(self.state, self.variant)

    def apply_move(self, move: Move) -> MoveResult:
        """Apply ``move`` to the current state and record history.

        Args:
            move: Move to apply.
        """
        before = self.state
        result = engine.apply_move(before, move, self.variant)
        self.state = result.state
        self._history.append(SessionStep(move=move, before=before, result=result))
        self._redo_stack.clear()
        return result

    def undo(self) -> SessionStep:
        """Undo the most recent move.

        Raises:
            InvalidMoveError: If there is no move to undo.
        """
        if not self._history:
            msg = "no moves to undo"
            raise InvalidMoveError(msg)

        step = self._history.pop()
        self.state = step.before
        self._redo_stack.append(step)
        return step

    def redo(self) -> MoveResult:
        """Redo the most recently undone move.

        Raises:
            InvalidMoveError: If there is no move to redo.
        """
        if not self._redo_stack:
            msg = "no moves to redo"
            raise InvalidMoveError(msg)

        undone_step = self._redo_stack.pop()
        before = self.state
        result = engine.apply_move(before, undone_step.move, self.variant)
        self.state = result.state
        self._history.append(SessionStep(move=undone_step.move, before=before, result=result))
        return result

    def clear_history(self) -> None:
        """Clear undo and redo history without changing the current state."""
        self._history.clear()
        self._redo_stack.clear()

history property

history: tuple[SessionStep, ...]

Return applied session steps in order.

redo_history property

redo_history: tuple[SessionStep, ...]

Return undone session steps available for redo, oldest first.

move_history property

move_history: tuple[Move, ...]

Return moves applied in this session.

result_history property

result_history: tuple[MoveResult, ...]

Return move results applied in this session.

last_result property

last_result: MoveResult | None

Return the most recent move result, if any.

can_undo property

can_undo: bool

Return whether the session has a move to undo.

can_redo property

can_redo: bool

Return whether the session has a move to redo.

__post_init__

__post_init__() -> None

Validate the starting state.

Source code in src/patiencepilot/game.py
43
44
45
def __post_init__(self) -> None:
    """Validate the starting state."""
    engine.validate_state(self.state, self.variant)

new classmethod

new(
    variant: Variant | str | None = None,
    *,
    seed: Seed = None,
    options: VariantOptions | None = None,
    metadata: Mapping[str, object] | None = None,
    ui_state: Mapping[str, object] | None = None,
) -> GameSession

Return a session around a newly dealt game.

Parameters:

Name Type Description Default
variant Variant | str | None

Rule set or registered variant name to use. Defaults to Klondike draw-one with unlimited redeals.

None
seed Seed

Optional deterministic random seed.

None
options VariantOptions | None

Variant options when variant is a name or omitted.

None
metadata Mapping[str, object] | None

Optional session metadata for callers or persistence.

None
ui_state Mapping[str, object] | None

Optional UI-only state for adapters.

None
Source code in src/patiencepilot/game.py
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
74
@classmethod
def new(
    cls,
    variant: Variant | str | None = None,
    *,
    seed: Seed = None,
    options: VariantOptions | None = None,
    metadata: Mapping[str, object] | None = None,
    ui_state: Mapping[str, object] | None = None,
) -> GameSession:
    """Return a session around a newly dealt game.

    Args:
        variant: Rule set or registered variant name to use. Defaults to
            Klondike draw-one with unlimited redeals.
        seed: Optional deterministic random seed.
        options: Variant options when ``variant`` is a name or omitted.
        metadata: Optional session metadata for callers or persistence.
        ui_state: Optional UI-only state for adapters.
    """
    state = engine.new_game(variant, seed=seed, options=options)
    return cls(
        state=state,
        variant=variant,
        seed=seed,
        metadata={} if metadata is None else dict(metadata),
        ui_state={} if ui_state is None else dict(ui_state),
    )

legal_moves

legal_moves() -> tuple[Move, ...]

Return legal moves from the current state.

Source code in src/patiencepilot/game.py
113
114
115
def legal_moves(self) -> tuple[Move, ...]:
    """Return legal moves from the current state."""
    return engine.legal_moves(self.state, self.variant)

validate_state

validate_state() -> None

Validate the current state.

Source code in src/patiencepilot/game.py
117
118
119
def validate_state(self) -> None:
    """Validate the current state."""
    engine.validate_state(self.state, self.variant)

is_won

is_won() -> bool

Return whether the current state is won.

Source code in src/patiencepilot/game.py
121
122
123
def is_won(self) -> bool:
    """Return whether the current state is won."""
    return engine.is_won(self.state, self.variant)

apply_move

apply_move(move: Move) -> MoveResult

Apply move to the current state and record history.

Parameters:

Name Type Description Default
move Move

Move to apply.

required
Source code in src/patiencepilot/game.py
125
126
127
128
129
130
131
132
133
134
135
136
def apply_move(self, move: Move) -> MoveResult:
    """Apply ``move`` to the current state and record history.

    Args:
        move: Move to apply.
    """
    before = self.state
    result = engine.apply_move(before, move, self.variant)
    self.state = result.state
    self._history.append(SessionStep(move=move, before=before, result=result))
    self._redo_stack.clear()
    return result

undo

undo() -> SessionStep

Undo the most recent move.

Raises:

Type Description
InvalidMoveError

If there is no move to undo.

Source code in src/patiencepilot/game.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def undo(self) -> SessionStep:
    """Undo the most recent move.

    Raises:
        InvalidMoveError: If there is no move to undo.
    """
    if not self._history:
        msg = "no moves to undo"
        raise InvalidMoveError(msg)

    step = self._history.pop()
    self.state = step.before
    self._redo_stack.append(step)
    return step

redo

redo() -> MoveResult

Redo the most recently undone move.

Raises:

Type Description
InvalidMoveError

If there is no move to redo.

Source code in src/patiencepilot/game.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def redo(self) -> MoveResult:
    """Redo the most recently undone move.

    Raises:
        InvalidMoveError: If there is no move to redo.
    """
    if not self._redo_stack:
        msg = "no moves to redo"
        raise InvalidMoveError(msg)

    undone_step = self._redo_stack.pop()
    before = self.state
    result = engine.apply_move(before, undone_step.move, self.variant)
    self.state = result.state
    self._history.append(SessionStep(move=undone_step.move, before=before, result=result))
    return result

clear_history

clear_history() -> None

Clear undo and redo history without changing the current state.

Source code in src/patiencepilot/game.py
170
171
172
173
def clear_history(self) -> None:
    """Clear undo and redo history without changing the current state."""
    self._history.clear()
    self._redo_stack.clear()

apply_move

apply_move(
    state: GameState,
    move: Move,
    variant: Variant | str | None = None,
) -> MoveResult

Apply move to state and return the resulting state and effects.

Source code in src/patiencepilot/engine.py
45
46
47
def apply_move(state: GameState, move: Move, variant: Variant | str | None = None) -> MoveResult:
    """Apply ``move`` to ``state`` and return the resulting state and effects."""
    return _resolve_variant(state, variant).apply_move(state, move)

is_won

is_won(
    state: GameState, variant: Variant | str | None = None
) -> bool

Return whether state is won.

Source code in src/patiencepilot/engine.py
50
51
52
def is_won(state: GameState, variant: Variant | str | None = None) -> bool:
    """Return whether ``state`` is won."""
    return _resolve_variant(state, variant).is_won(state)

legal_moves

legal_moves(
    state: GameState, variant: Variant | str | None = None
) -> tuple[Move, ...]

Return legal moves available from state.

Source code in src/patiencepilot/engine.py
40
41
42
def legal_moves(state: GameState, variant: Variant | str | None = None) -> tuple[Move, ...]:
    """Return legal moves available from ``state``."""
    return _resolve_variant(state, variant).legal_moves(state)

new_game

new_game(
    variant: Variant | str | None = None,
    seed: Seed = None,
    options: VariantOptions | None = None,
) -> GameState

Return a new game state.

Parameters:

Name Type Description Default
variant Variant | str | None

Rule set or registered variant name to use. Defaults to Klondike draw-one with unlimited redeals.

None
seed Seed

Optional deterministic random seed.

None
options VariantOptions | None

Variant options when variant is a name or omitted.

None
Source code in src/patiencepilot/engine.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def new_game(
    variant: Variant | str | None = None,
    seed: Seed = None,
    options: VariantOptions | None = None,
) -> GameState:
    """Return a new game state.

    Args:
        variant: Rule set or registered variant name to use. Defaults to
            Klondike draw-one with unlimited redeals.
        seed: Optional deterministic random seed.
        options: Variant options when ``variant`` is a name or omitted.
    """
    rules = _coerce_variant(variant, options)
    return rules.new_game(seed=seed)

validate_state

validate_state(
    state: GameState, variant: Variant | str | None = None
) -> None

Validate that state is coherent for its variant.

Parameters:

Name Type Description Default
state GameState

State to validate.

required
variant Variant | str | None

Optional explicit rule set or registered variant name. When omitted, rules are resolved from state metadata.

None
Source code in src/patiencepilot/engine.py
29
30
31
32
33
34
35
36
37
def validate_state(state: GameState, variant: Variant | str | None = None) -> None:
    """Validate that ``state`` is coherent for its variant.

    Args:
        state: State to validate.
        variant: Optional explicit rule set or registered variant name. When
            omitted, rules are resolved from state metadata.
    """
    _resolve_variant(state, variant).validate_state(state)

new_klondike_game

new_klondike_game(
    *,
    seed: Seed = None,
    draw_count: int = 1,
    redeals: int | None = None,
) -> GameState

Return a new Klondike game state.

Parameters:

Name Type Description Default
seed Seed

Optional deterministic random seed.

None
draw_count int

Number of cards drawn from stock at once.

1
redeals int | None

Number of waste-to-stock redeals allowed, or None for unlimited redeals.

None
Source code in src/patiencepilot/game.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def new_klondike_game(
    *,
    seed: Seed = None,
    draw_count: int = 1,
    redeals: int | None = None,
) -> GameState:
    """Return a new Klondike game state.

    Args:
        seed: Optional deterministic random seed.
        draw_count: Number of cards drawn from stock at once.
        redeals: Number of waste-to-stock redeals allowed, or ``None`` for
            unlimited redeals.
    """
    return new_game("klondike", seed=seed, options={"draw_count": draw_count, "redeals": redeals})