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)]