Skip to content

Base

patiencepilot.solvers.base

Solver visibility and advice contracts.

SearchLimit dataclass

Optional limits for bounded solver work.

Source code in src/patiencepilot/solvers/base.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@dataclass(frozen=True, slots=True)
class SearchLimit:
    """Optional limits for bounded solver work."""

    time_seconds: float | None = None
    node_limit: int | None = None
    depth_limit: int | None = None

    def __post_init__(self) -> None:
        """Validate search-limit values."""
        if self.time_seconds is not None and self.time_seconds <= 0:
            msg = "time_seconds must be positive when provided"
            raise InvalidStateError(msg)
        if self.node_limit is not None and self.node_limit < 1:
            msg = "node_limit must be at least 1 when provided"
            raise InvalidStateError(msg)
        if self.depth_limit is not None and self.depth_limit < 0:
            msg = "depth_limit must be non-negative when provided"
            raise InvalidStateError(msg)

__post_init__

__post_init__() -> None

Validate search-limit values.

Source code in src/patiencepilot/solvers/base.py
21
22
23
24
25
26
27
28
29
30
31
def __post_init__(self) -> None:
    """Validate search-limit values."""
    if self.time_seconds is not None and self.time_seconds <= 0:
        msg = "time_seconds must be positive when provided"
        raise InvalidStateError(msg)
    if self.node_limit is not None and self.node_limit < 1:
        msg = "node_limit must be at least 1 when provided"
        raise InvalidStateError(msg)
    if self.depth_limit is not None and self.depth_limit < 0:
        msg = "depth_limit must be non-negative when provided"
        raise InvalidStateError(msg)

RankedMove dataclass

One ranked solver move recommendation.

Source code in src/patiencepilot/solvers/base.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@dataclass(frozen=True, slots=True)
class RankedMove:
    """One ranked solver move recommendation."""

    move: Move
    rank: int = 1
    score: float | None = None
    confidence: float | None = None
    reason: str | None = None
    tags: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        """Validate ranked-move metadata."""
        if self.rank < 1:
            msg = "rank must be at least 1"
            raise InvalidStateError(msg)
        if self.confidence is not None and not 0 <= self.confidence <= 1:
            msg = "confidence must be between 0 and 1"
            raise InvalidStateError(msg)

__post_init__

__post_init__() -> None

Validate ranked-move metadata.

Source code in src/patiencepilot/solvers/base.py
45
46
47
48
49
50
51
52
def __post_init__(self) -> None:
    """Validate ranked-move metadata."""
    if self.rank < 1:
        msg = "rank must be at least 1"
        raise InvalidStateError(msg)
    if self.confidence is not None and not 0 <= self.confidence <= 1:
        msg = "confidence must be between 0 and 1"
        raise InvalidStateError(msg)

Advice dataclass

Solver advice containing zero or more ranked move alternatives.

Source code in src/patiencepilot/solvers/base.py
 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
@dataclass(frozen=True, slots=True)
class Advice:
    """Solver advice containing zero or more ranked move alternatives."""

    recommendations: tuple[RankedMove, ...]
    solver_name: str | None = None
    limit: SearchLimit | None = None
    nodes_searched: int | None = None
    depth_reached: int | None = None
    elapsed_seconds: float | None = None
    assumptions: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        """Sort and validate advice metadata."""
        object.__setattr__(self, "recommendations", tuple(sorted(self.recommendations, key=lambda item: item.rank)))
        if self.nodes_searched is not None and self.nodes_searched < 0:
            msg = "nodes_searched must be non-negative"
            raise InvalidStateError(msg)
        if self.depth_reached is not None and self.depth_reached < 0:
            msg = "depth_reached must be non-negative"
            raise InvalidStateError(msg)
        if self.elapsed_seconds is not None and self.elapsed_seconds < 0:
            msg = "elapsed_seconds must be non-negative"
            raise InvalidStateError(msg)

    @classmethod
    def from_move(
        cls,
        move: Move,
        *,
        solver_name: str | None = None,
        limit: SearchLimit | None = None,
        score: float | None = None,
        confidence: float | None = None,
        reason: str | None = None,
    ) -> Advice:
        """Return advice with a single recommended move.

        Args:
            move: Recommended move.
            solver_name: Optional solver identifier.
            limit: Optional search limits used for the recommendation.
            score: Optional solver-specific move score.
            confidence: Optional confidence from 0 to 1.
            reason: Optional human-readable reason.
        """
        return cls(
            recommendations=(RankedMove(move=move, rank=1, score=score, confidence=confidence, reason=reason),),
            solver_name=solver_name,
            limit=limit,
        )

    @property
    def best(self) -> RankedMove | None:
        """Return the highest-ranked recommendation, if any."""
        if not self.recommendations:
            return None
        return self.recommendations[0]

    @property
    def best_move(self) -> Move | None:
        """Return the highest-ranked move, if any."""
        best = self.best
        if best is None:
            return None
        return best.move

    @property
    def alternatives(self) -> tuple[RankedMove, ...]:
        """Return recommendations after the best move."""
        return self.recommendations[1:]

best property

best: RankedMove | None

Return the highest-ranked recommendation, if any.

best_move property

best_move: Move | None

Return the highest-ranked move, if any.

alternatives property

alternatives: tuple[RankedMove, ...]

Return recommendations after the best move.

__post_init__

__post_init__() -> None

Sort and validate advice metadata.

Source code in src/patiencepilot/solvers/base.py
67
68
69
70
71
72
73
74
75
76
77
78
def __post_init__(self) -> None:
    """Sort and validate advice metadata."""
    object.__setattr__(self, "recommendations", tuple(sorted(self.recommendations, key=lambda item: item.rank)))
    if self.nodes_searched is not None and self.nodes_searched < 0:
        msg = "nodes_searched must be non-negative"
        raise InvalidStateError(msg)
    if self.depth_reached is not None and self.depth_reached < 0:
        msg = "depth_reached must be non-negative"
        raise InvalidStateError(msg)
    if self.elapsed_seconds is not None and self.elapsed_seconds < 0:
        msg = "elapsed_seconds must be non-negative"
        raise InvalidStateError(msg)

from_move classmethod

from_move(
    move: Move,
    *,
    solver_name: str | None = None,
    limit: SearchLimit | None = None,
    score: float | None = None,
    confidence: float | None = None,
    reason: str | None = None,
) -> Advice

Return advice with a single recommended move.

Parameters:

Name Type Description Default
move Move

Recommended move.

required
solver_name str | None

Optional solver identifier.

None
limit SearchLimit | None

Optional search limits used for the recommendation.

None
score float | None

Optional solver-specific move score.

None
confidence float | None

Optional confidence from 0 to 1.

None
reason str | None

Optional human-readable reason.

None
Source code in src/patiencepilot/solvers/base.py
 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
@classmethod
def from_move(
    cls,
    move: Move,
    *,
    solver_name: str | None = None,
    limit: SearchLimit | None = None,
    score: float | None = None,
    confidence: float | None = None,
    reason: str | None = None,
) -> Advice:
    """Return advice with a single recommended move.

    Args:
        move: Recommended move.
        solver_name: Optional solver identifier.
        limit: Optional search limits used for the recommendation.
        score: Optional solver-specific move score.
        confidence: Optional confidence from 0 to 1.
        reason: Optional human-readable reason.
    """
    return cls(
        recommendations=(RankedMove(move=move, rank=1, score=score, confidence=confidence, reason=reason),),
        solver_name=solver_name,
        limit=limit,
    )

AdviceProvider

Bases: Protocol

Protocol for components that produce advice from player-known state.

Source code in src/patiencepilot/solvers/base.py
128
129
130
131
132
133
class AdviceProvider(Protocol):
    """Protocol for components that produce advice from player-known state."""

    def suggest(self, view: PlayerView, *, limit: SearchLimit | None = None) -> Advice:
        """Return move advice using only the supplied player-known view."""
        ...

suggest

suggest(
    view: PlayerView, *, limit: SearchLimit | None = None
) -> Advice

Return move advice using only the supplied player-known view.

Source code in src/patiencepilot/solvers/base.py
131
132
133
def suggest(self, view: PlayerView, *, limit: SearchLimit | None = None) -> Advice:
    """Return move advice using only the supplied player-known view."""
    ...

Solver

Bases: AdviceProvider, Protocol

Protocol for pluggable Solitaire solvers.

Source code in src/patiencepilot/solvers/base.py
136
137
138
139
class Solver(AdviceProvider, Protocol):
    """Protocol for pluggable Solitaire solvers."""

    name: str