Source code for imperfecto.games.game
# -*- coding: utf-8 -*-
"""A collection of base game classes.
Classes:
    * ExtensiveFormGame
    * NormalFormGame
"""
from abc import ABC, abstractmethod
from enum import EnumMeta, IntEnum
from typing import Sequence, Tuple
from imperfecto.algos.player import Player
[docs]class ExtensiveFormGame(ABC):
    """Abstract class for extensive form games.
    In an extensive form game, players have some private information, and are unsure
    about the true state of the world.
    Note:
        ``ExtensiveFormGame`` subclass must have class-level attribute ``actions``, and
        ``n_players``.
    Args:
        players: The players of the game.
    Attributes:
        actions: The actions of the game (class-level attribute).
        n_players: The number of players in the game (class-level attribute).
        players (Sequence[Player]): The players of the game.
    """
    n_players: int
    actions: EnumMeta
    def __init__(self, players: Sequence[Player]):
        self.players = players
    @property
    def players(self) -> Sequence[Player]:
        """The players of the game."""
        return self._players
    @players.setter
    def players(self, val: Sequence[Player]) -> None:
        self._players = val
        # setting game for each player
        for player in self._players:
            player.game = self
[docs]    @abstractmethod
    def get_active_player(self, history: Sequence[IntEnum]) -> Player:
        """Get the active player of the game at the current decision point.
        Args:
            history: The history of the game.
        Returns:
            The active player of the game at the current decision point.
        """
        pass
[docs]    @abstractmethod
    def is_terminal(self, history: Sequence[IntEnum]) -> bool:
        """Check if the game is in a terminal state.
        Args:
            history: The history of the game.
        Returns:
            True if the game is in a terminal state, False otherwise.
        """
        pass
[docs]    @abstractmethod
    def get_payoffs(self, history: Sequence[IntEnum]) -> Sequence[float]:
        """Return the payoff for each player at the current node.
        Note:
            history must be a terminal node.
        Args:
            history: The history of the game.
        Returns:
            The payoffs of the players at the end of the game.
        """
        pass
[docs]    @abstractmethod
    def get_infostate(self, history: Sequence[IntEnum]) -> str:
        """Return the infostate (i.e. the information set) of the game.
        Args:
            history: The history of the game.
        Returns:
            A string representation of the infostate of the game.
        """
        pass
[docs]    def history_to_str(self, history: Sequence[IntEnum]) -> str:
        """Return a string representation of the history of the game.
        Args:
            history: The history of the game.
        Returns:
            A string representation of the history of the game.
        """
        return '-'.join(str(action) for action in history)
[docs]    def play(self) -> Tuple[Sequence[IntEnum], Sequence[float]]:
        """
        Play the game with the current players and their strategies and return the payoffs.
        Returns:
            A tuple of the play-out history of the game and the payoffs of the players.
        """
        history = []
        while not self.is_terminal(history):
            active_player = self.get_active_player(history)
            infostate = self.get_infostate(history)
            action = getattr(self, "actions")(active_player.act(
                infostate))  # convert int to action enum
            history.append(action)
        return history, self.get_payoffs(history)
[docs]    @staticmethod
    def shorten_history(history_str: str) -> str:
        """Shorten history string. Games with long action names should
        override this method.
        Args:
            history_str: history string to shorten.
        Returns:
            a shortened history string.
        """
        return history_str
[docs]class NormalFormGame(ExtensiveFormGame, ABC):
    """N-player normal form game.
    This class of game is a special form of extensive-form games. A normal form game involves every
    player making simultaneous moves. Thus, they are unsure about each other's move.
    Args:
        players: The players of the game.
    """
    def __init__(self, players: Sequence[Player]):
        assert len(players) == self.n_players
        super().__init__(players)
[docs]    def is_terminal(self, history: Sequence[IntEnum]) -> bool:
        return len(history) == self.n_players
[docs]    def get_infostate(self, history: Sequence[IntEnum]) -> str:
        info_str_dict = {i: f"P{i}" for i in range(self.n_players)}
        if len(history) not in range(self.n_players):
            raise ValueError("Invalid history " + str(history))
        return info_str_dict[len(history)]
[docs]    def get_active_player(self, history: Sequence[IntEnum]) -> Player:
        if len(history) not in range(self.n_players):
            raise ValueError("Invalid history " + str(history))
        return self.players[len(history)]