import warnings
from abc import ABC, ABCMeta, abstractmethod
from typing import TYPE_CHECKING, Optional
import uuid
from .enums import ActionType
from .playeraction import PlayerAction
from .playerstate import PlayerState
from ._registered_players import registered_players
if TYPE_CHECKING: # pragma: no cover
from .game import Game
from .events import GameEvent
__all__ = ["Player"]
# Registry for classes by their cls_uid
_uid_registry: dict[str, type["Player"]] = {}
class PlayerMeta(ABCMeta):
def __init__(self, name, bases, namespace, *args, **kwargs):
super().__init__(name, bases, namespace, *args, **kwargs)
def __new__(metaclass, name, bases, namespace, *args, **kwargs):
cls = super().__new__(metaclass, name, bases, namespace, *args, **kwargs)
if not ABC in bases and getattr(cls, "register", True):
registered_players[cls.__name__] = cls
# Register by cls_uid if it exists
cls_uid = getattr(cls, "cls_uid", None)
if cls_uid is not None:
_uid_registry[cls_uid] = cls
return cls
[docs]
class Player(metaclass=PlayerMeta):
"""Abstract base class for a poker player."""
register: bool = True
cls_uid: Optional[str] = None
[docs]
def __init__(
self,
*,
uid: Optional[str] = None,
name: str,
state: Optional[PlayerState | dict] = None,
**kwargs,
):
_id = kwargs.pop("id", None)
if _id:
warnings.warn(
"Passing id= to Player.__init__ is deprecated, use uid= instead.",
DeprecationWarning,
stacklevel=2,
)
self.uid = uid or _id or uuid.uuid4().hex
self.name = name
self.state = (
state
if isinstance(state, PlayerState) or state is None
else PlayerState.model_validate(state)
)
@property
def id(self) -> str:
"""Deprecated: use ``uid`` instead."""
warnings.warn(
"Player.id is deprecated, use Player.uid instead.",
DeprecationWarning,
stacklevel=2,
)
return self.uid
[docs]
@classmethod
def get_by_uid(cls, uid: str) -> Optional[type["Player"]]:
"""
Get a player class by its unique identifier.
Parameters
----------
uid : str
The unique identifier of the player class.
Returns
-------
Optional[type[Player]]
The player class with the given uid, or None if not found.
"""
return _uid_registry.get(uid)
[docs]
@abstractmethod
def decide_action(
self,
*,
game: "Game",
valid_actions: list[ActionType],
min_raise_amount: int,
call_amount: int,
min_bet_amount: int,
) -> PlayerAction:
"""
Decide on an action to take during the player's turn.
The function should return a valid instance of PlayerAction.
Parameters
----------
game : Game
The game instance containing the current state.
valid_actions : list[ActionType]
List of valid actions the player can take.
min_raise_amount : int
Minimum extra chips this player must add right now to complete a minimum raise.
call_amount : int
Amount of chips this player must add right now to call the current bet.
min_bet_amount : int
Minimum chips this player must add right now to make a bet.
Returns
-------
PlayerAction
An instance of PlayerAction representing the chosen action.
"""
...
[docs]
def on_event(self, event: "GameEvent", game: "Game") -> None: # pragma: no cover
"""
Optional hook called when a game event occurs.
This method is called synchronously after global event handlers.
Exceptions in this method are caught and logged by the engine.
Parameters
----------
event : GameEvent
The game event that occurred.
game : Game
The game instance containing the current state.
Notes
-----
This is an optional hook. The default implementation does nothing.
Subclasses can override this method to observe events.
"""
...
[docs]
def to_dict(self) -> dict:
"""
Serialize the player to a dictionary.
Returns
-------
dict
A dictionary representation of the player.
"""
return {
"uid": self.uid,
"name": self.name,
"state": self.state.model_dump() if self.state else None,
}