"""
Utilities for converting between representations of games.
Currently supports reading and writing the GameTracer `.gam` text format
[1]_.
Examples
--------
Create a QuantEcon NormalFormGame from a .gam file storing a 3-player
Minimum Effort Game:
>>> import os
>>> import quantecon.game_theory as gt
>>> filepath = os.path.dirname(gt.__file__)
>>> filepath = os.path.join(filepath, 'tests', 'game_files',
... 'minimum_effort_game.gam')
>>> nfg = gt.from_gam(filepath)
>>> print(nfg)
3-player NormalFormGame with payoff profile array:
[[[[ 1., 1., 1.], [ 1., 1., -9.], [ 1., 1., -19.]],
[[ 1., -9., 1.], [ 1., -9., -9.], [ 1., -9., -19.]],
[[ 1., -19., 1.], [ 1., -19., -9.], [ 1., -19., -19.]]],
<BLANKLINE>
[[[ -9., 1., 1.], [ -9., 1., -9.], [ -9., 1., -19.]],
[[ -9., -9., 1.], [ 2., 2., 2.], [ 2., 2., -8.]],
[[ -9., -19., 1.], [ 2., -8., 2.], [ 2., -8., -8.]]],
<BLANKLINE>
[[[-19., 1., 1.], [-19., 1., -9.], [-19., 1., -19.]],
[[-19., -9., 1.], [ -8., 2., 2.], [ -8., 2., -8.]],
[[-19., -19., 1.], [ -8., -8., 2.], [ 3., 3., 3.]]]]
References
----------
.. [1] Ben Blum, Daphne Koller, Christian Shelton, "Game Theory:
GameTracer," http://dags.stanford.edu/Games/gametracer.html
"""
import io
import sys
import numbers
import numpy as np
from .normal_form_game import Player, NormalFormGame
[docs]class GAMPayoffVector:
"""
Internal intermediate representation that stores payoffs in a single
flat 1-dim array.
Payoff values are ordered as in the GameTracer .gam format:
1. Player-major blocks: player 0, ..., player N-1.
2. Within each block, action profiles are ordered with player 0
varying fastest, then player 1, ..., player N-1 (i.e.,
Fortran/column-major order).
Attributes
----------
N : scalar(int)
Number of players.
nums_actions : tuple(int)
Tuple of the numbers of actions, one for each player.
payoffs : ndarray(ndim=1)
Array storing payoffs in .gam order.
"""
def __init__(self, nums_actions, payoffs):
nums_actions = tuple(nums_actions)
if len(nums_actions) == 0:
raise ValueError('nums_actions must be a non-empty iterable ' +
'of positive integers')
for n in nums_actions:
if not isinstance(n, numbers.Integral):
raise TypeError('nums_actions must contain only integers')
if n <= 0:
raise ValueError('all nums_actions must be positive')
self.nums_actions = tuple(int(n) for n in nums_actions)
self.N = len(self.nums_actions)
payoffs = np.ascontiguousarray(payoffs)
if payoffs.ndim != 1:
raise ValueError('payoffs must be a 1-dim array_like')
expected = np.prod(self.nums_actions) * self.N
if payoffs.size != expected:
raise ValueError(
f'payoffs length mismatch: expected {expected}, ' +
f'got {payoffs.size}'
)
self.payoffs = payoffs
[docs] @classmethod
def from_nfg(cls, g, dtype=None):
"""
Construct a GAMPayoffVector from a NormalFormGame `g`.
Examples
--------
>>> player0 = Player([[0, 3], [1, 4], [2, 5]])
>>> player1 = Player([[6, 7, 8], [9, 10, 11]])
>>> g = NormalFormGame((player0, player1))
>>> print(g)
2-player NormalFormGame with payoff profile array:
[[[ 0, 6], [ 3, 9]],
[[ 1, 7], [ 4, 10]],
[[ 2, 8], [ 5, 11]]]
>>> p = GAMPayoffVector.from_nfg(g)
>>> p.payoffs
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
"""
N = g.N
nums_actions = g.nums_actions
if dtype is None:
dtype = g.dtype
na = np.prod(nums_actions)
payoffs = np.empty(na*N, dtype=dtype)
for i, player in enumerate(g.players):
payoffs[na*i:na*(i+1)].reshape(nums_actions, order='F')[:] = \
player.payoff_array.transpose(
(*range(N-i, g.N), *range(N-i))
)
return cls(nums_actions, payoffs)
[docs] def to_nfg(self, dtype=None):
"""
Construct a NormalFormGame from self.
Examples
--------
>>> nums_actions = (3, 2)
>>> payoffs = np.arange(12)
>>> p = GAMPayoffVector(nums_actions, payoffs)
>>> g = p.to_nfg()
>>> print(g)
2-player NormalFormGame with payoff profile array:
[[[ 0, 6], [ 3, 9]],
[[ 1, 7], [ 4, 10]],
[[ 2, 8], [ 5, 11]]]
"""
N = self.N
nums_actions = self.nums_actions
na = np.prod(nums_actions)
payoffs2d = self.payoffs.reshape((na, N), order='F')
players = tuple(
Player(
np.asarray(
payoffs2d[:, i].reshape(nums_actions, order='F').transpose(
(*range(i, N), *range(i))
), dtype=dtype, order='C'
)
) for i in range(N)
)
return NormalFormGame(players)
def _str2num(s):
"""
Convert string to appropriate numeric type.
Parameters
----------
s : str
String representation of a number.
Returns
-------
int or float
Integer if no decimal point, otherwise float.
"""
if '.' in s:
return float(s)
return int(s)
[docs]class GAMReader:
"""
Parser for the GameTracer .gam format.
"""
[docs] @classmethod
def from_file(cls, file_path):
"""
Read from a .gam format file.
"""
with open(file_path, 'r') as f:
string = f.read()
return cls._parse(string)
[docs] @classmethod
def from_url(cls, url):
"""
Read from a URL.
"""
import urllib.request
with urllib.request.urlopen(url) as response:
string = response.read().decode()
return cls._parse(string)
[docs] @classmethod
def from_string(cls, string):
"""
Read from a .gam format string.
"""
return cls._parse(string)
@staticmethod
def _parse(string):
tokens = string.split()
if not tokens:
raise ValueError('empty .gam input')
pos = 0
# N
tok = tokens[pos]
try:
N = int(tok)
except ValueError as err:
raise ValueError(f'invalid N token: {tok!r}') from err
pos += 1
if N <= 0:
raise ValueError('N must be a positive integer')
# nums_actions
if len(tokens) < pos + N:
got = max(0, len(tokens) - pos)
raise ValueError(
f'incomplete header: expected {N} action counts, got {got}'
)
try:
nums_actions = tuple(int(tok) for tok in tokens[pos:pos+N])
except ValueError as err:
raise ValueError('invalid action count token in header') from err
pos += N
# payoffs
payoffs = np.array([_str2num(tok) for tok in tokens[pos:]])
p = GAMPayoffVector(nums_actions, payoffs)
return p.to_nfg()
[docs]class GAMWriter:
"""
Serializer for the GameTracer .gam format.
"""
[docs] @classmethod
def to_file(cls, g, file_path):
"""
Write `g` to a file in GameTracer .gam format.
"""
with open(file_path, 'w') as f:
f.write(cls._dump(g) + '\n')
[docs] @classmethod
def to_string(cls, g):
"""
Return the GameTracer .gam string representation of `g`.
"""
return cls._dump(g)
@staticmethod
def _dump(g):
p = GAMPayoffVector.from_nfg(g)
buf = io.StringIO()
buf.write(str(p.N))
buf.write('\n')
buf.write(' '.join(map(str, p.nums_actions)))
buf.write('\n\n')
payoffs_str = np.array2string(
p.payoffs,
separator=' ',
threshold=sys.maxsize, # no truncation '...'
# suppress_small helps avoid scientific notation for small |x|;
# large |x| values may still print with e+...
suppress_small=True
)[1:-1] # strip brackets
buf.write(' '.join(payoffs_str.split()))
return buf.getvalue().rstrip()
[docs]def from_gam(filename: str) -> NormalFormGame:
"""
Read a GameTracer .gam file and return a NormalFormGame.
Parameters
----------
filename : str
Path to .gam file.
Returns
-------
NormalFormGame
The game described by the .gam file.
Examples
--------
Save a .gam format string in a temporary file:
>>> import tempfile
>>> fname = tempfile.mkstemp()[1]
>>> with open(fname, mode='w') as f:
... _ = f.write(\"\"\"\\
... 2
... 3 2
...
... 3 2 0 3 5 6 3 2 3 2 6 1\"\"\")
Read the file:
>>> g = from_gam(fname)
>>> print(g)
2-player NormalFormGame with payoff profile array:
[[[3, 3], [3, 2]],
[[2, 2], [5, 6]],
[[0, 3], [6, 1]]]
"""
return GAMReader.from_file(filename)
[docs]def from_gam_string(string):
"""
Read a .gam format string and return a NormalFormGame.
Parameters
----------
string : str
String in .gam format.
Returns
-------
NormalFormGame
The game described by the .gam string.
Examples
--------
>>> string = \"\"\"\\
... 2
... 3 2
...
... 3 2 0 3 5 6 3 2 3 2 6 1\"\"\"
>>> g = from_gam_string(string)
>>> print(g)
2-player NormalFormGame with payoff profile array:
[[[3, 3], [3, 2]],
[[2, 2], [5, 6]],
[[0, 3], [6, 1]]]
"""
return GAMReader.from_string(string)
[docs]def from_gam_url(url):
"""
Read a GameTracer .gam file from a URL and return a NormalFormGame.
Parameters
----------
url : str
String containing a URL of the .gam file.
Returns
-------
NormalFormGame
The game described by the .gam file.
"""
return GAMReader.from_url(url)
[docs]def to_gam(g, file_path=None):
"""
Write a NormalFormGame to a file in .gam format.
Parameters
----------
g : NormalFormGame
file_path : str, optional(default=None)
Path to the file to write to. If None, the result is returned as
a string.
Returns
-------
None or str
"""
if file_path is None:
return GAMWriter.to_string(g)
return GAMWriter.to_file(g, file_path)