Source code for gameanalysis.gamegen

"""Module for generating random games"""
import itertools
from collections import abc

import numpy as np
import numpy.random as rand
import scipy.special as sps

from gameanalysis import aggfn
from gameanalysis import matgame
from gameanalysis import paygame
from gameanalysis import rsgame
from gameanalysis import utils


[docs]def default_distribution(shape=None): """Default distribution for payoffs""" return rand.uniform(-1, 1, shape)
[docs]def gen_profiles(base, prob=1.0, distribution=default_distribution): """Generate profiles given game structure Parameters ---------- base : RsGame Game to generate payoffs for. prob : float, optional The probability to add a profile from the full game. distribution : (shape) -> ndarray, optional Distribution function to draw payoffs from. """ # First turn input into number of profiles to compute num_profs = base.num_all_profiles utils.check(0 <= prob <= 1, "probability must be in [0, 1] but was {:g}", prob) if num_profs <= np.iinfo(int).max: num = rand.binomial(num_profs, prob) else: num = round(float(num_profs * prob)) return gen_num_profiles(base, num, distribution)
[docs]def gen_num_profiles(base, num, distribution=default_distribution): """Generate profiles given game structure Parameters ---------- base : RsGame Game to generate payoffs for. count : int The number of profiles to generate. distribution : (shape) -> ndarray, optional Distribution function to draw payoffs from. """ utils.check( 0 <= num <= base.num_all_profiles, "num must be in [0, {:d}] but was {:d}", base.num_all_profiles, num, ) profiles = sample_profiles(base, num) payoffs = np.zeros(profiles.shape) mask = profiles > 0 payoffs[mask] = distribution(mask.sum()) return paygame.game_replace(base, profiles, payoffs)
[docs]def game(players, strats, prob=1.0, distribution=default_distribution): """Generate a random role symmetric game with sparse profiles Parameters ---------- players : int or [int] The number of players per role. strats : int or [int] The number of strategies per role. prob : float, optional The probability of any profile being included in the game. distribution : (shape) -> ndarray, optional Distribution function to draw payoffs from. """ return game_replace(rsgame.empty(players, strats), prob, distribution)
[docs]def game_replace(base, prob=1.0, distribution=default_distribution): """Replace a games profiles with random ones This is identical to gen_profiles, but provides a more common name.""" return gen_profiles(base, prob, distribution)
[docs]def sparse_game(players, strats, num, distribution=default_distribution): """Generate a random role symmetric game with sparse profiles Parameters ---------- players : int or [int] The number of players per role. strats : int or [int] The number of strategies per role. num : int The number of profiles to draw payoffs for. distribution : (shape) -> ndarray, optional Distribution function to draw payoffs from. """ return gen_num_profiles(rsgame.empty(players, strats), num, distribution)
[docs]def width_gaussian(widths, num_samples): """Gaussian width distribution Samples come from iid Gaussian distributions. """ return rand.normal(0, widths[:, None], (widths.size, num_samples))
[docs]def width_bimodal(widths, num_samples): """Bimodal Gaussian width distribution Samples come from a uniform mixture between two symmetric Gaussians. """ squared_widths = widths ** 2 variances = rand.uniform(0, squared_widths) sdevs = np.sqrt(variances)[:, None] spreads = np.sqrt(squared_widths - variances)[:, None] draws = rand.normal(spreads, sdevs, (widths.size, num_samples)) draws *= rand.randint(0, 2, draws.shape) * 2 - 1 return draws
[docs]def width_uniform(widths, num_samples): """Uniform width distribution Samples come from iid uniform distributions. """ halfwidths = np.sqrt(3) * widths[:, None] return rand.uniform(-halfwidths, halfwidths, (widths.size, num_samples))
[docs]def width_gumbel(widths, num_samples): """Gumbel width distribution Samples come from iid Gumbel distributions. Distributions are randomly inverted since Gumbels are skewed. """ scales = widths[:, None] * np.sqrt(6) / np.pi draws = np.random.gumbel( -scales * np.euler_gamma, scales, (widths.size, num_samples) ) draws *= rand.randint(0, 2, (widths.size, 1)) * 2 - 1 return draws
[docs]def gen_noise( # pylint: disable=too-many-arguments,too-many-locals base, prob=0.5, min_samples=1, min_width=0, max_width=1, noise_distribution=width_gaussian, ): """Generate noise for profiles of a game This generates samples for payoff data by first generating some measure of distribution spread for each payoff in the game. Then, for each sample, noise is drawn from this distribution. As a result, some payoffs will have significantly more noise than other payoffs, helping to mimic real games. Parameters ---------- base : Game The game to generate samples from. These samples are additive zero-mean noise to the payoff values. prob : float, optional The probability of continuing to add another sample to a profile. If this is 0, min_samples will be generated for each profile. As this approaches one, more samples will be generated for each profile, sampled from the geometric distribution of 1 - prob. min_samples : int, optional The minimum number of samples to generate for each profile. By default this will generate at least one sample for every profile with data. Setting this to zero means that a profile will only have data with probability `prob`. min_width : float, optional The minimum standard deviation of each payoffs samples. max_width : float, optional The maximum standard deviation of each payoffs samples. noise_distribution : width distribution, optional The noise generating function to use. This function must be a valid width noise distribution. A width distribution takes an array of widths and a number of samples, and draws that many samples for each width such that the standard deviation of the samples is equal to the width and the mean is zero. Several default versions are specified in gamegen, and they're all prefixed with `width_`. By default, this uses `width_gaussian`. """ if base.is_empty(): return paygame.samplegame_copy(base) perm = rand.permutation(base.num_profiles) profiles = base.profiles()[perm] payoffs = base.payoffs()[perm] samples = utils.geometric_histogram(profiles.shape[0], 1 - prob) mask = samples > 0 observations = np.arange(samples.size)[mask] + min_samples splits = samples[mask][:-1].cumsum() sample_payoffs = [] new_profiles = [] for num, prof, pay in zip( observations, np.split(profiles, splits), np.split(payoffs, splits) ): if num == 0: continue supp = prof > 0 spay = np.zeros((pay.shape[0], num, base.num_strats)) pview = np.rollaxis(spay, 1, 3) widths = rand.uniform(min_width, max_width, supp.sum()) pview[supp] = pay[supp, None] + noise_distribution(widths, num) new_profiles.append(prof) sample_payoffs.append(spay) if new_profiles: new_profiles = np.concatenate(new_profiles) else: # No data new_profiles = np.empty((0, base.num_strats), dtype=int) return paygame.samplegame_replace(base, new_profiles, sample_payoffs)
[docs]def samplegame(players, strats, *args, **kwargs): """Generate a random role symmetric sample game Parameters ---------- players : int or [int] The number of players per role. strats : int or [int] The number of strategies per role. **args The arguments to pass to samplegame_replace. """ return samplegame_replace(rsgame.empty(players, strats), *args, **kwargs)
[docs]def samplegame_replace( # pylint: disable=too-many-arguments base, prob=0.5, min_samples=1, min_width=0, max_width=1, payoff_distribution=default_distribution, noise_distribution=width_gaussian, ): """Generate a random role symmetric sample game Parameters ---------- base : RsGame The structure of the game to generate. prob : float, optional The probability of adding another sample above min_samples. These draws are repeated, so 0.5 will add one extra sample in expectation. min_samples : int, optional The minimum number of samples to generate for each profile. If 0, the game will potentially be sparse. min_width : float, optional The minimum standard deviation for each payoffs samples. max_width : float, optional The maximum standard deviation for each payoffs samples. payoff_distribution : (shape) -> ndarray, optional The distribution to sample mean payoffs from. noise_distribution : width distribution, optional The distribution used to add noise to each payoff. See `gen_noise` for a description of width distributions. """ if min_samples == 0: profs = game_replace(base, prob, payoff_distribution) min_samples = 1 else: profs = game_replace(base, distribution=payoff_distribution) return gen_noise(profs, prob, min_samples, min_width, max_width, noise_distribution)
[docs]def independent_game(num_role_strats, distribution=default_distribution): """Generate a random independent (asymmetric) game All payoffs are generated independently from distribution. Parameters ---------- num_role_strats : int or [int], len == num_role_players The number of strategies for each player. If an int, then it's a one player game. distribution : (shape) -> ndarray (shape) The distribution to sample payoffs from. Must take a single shape argument and return an ndarray of iid values with that shape. """ num_role_strats = np.asarray(num_role_strats, int) shape = np.append(num_role_strats, num_role_strats.size) return matgame.matgame(distribution(shape))
[docs]def covariant_game( num_role_strats, mean_dist=np.zeros, var_dist=np.ones, covar_dist=default_distribution, ): """Generate a covariant game Covariant games are asymmetric games where payoff values for each profile drawn according to multivariate normal. The multivariate normal for each profile has a constant mean drawn from `mean_dist`, constant variance drawn from`var_dist`, and constant covariance drawn from `covar_dist`. Parameters ---------- mean_dist : (shape) -> ndarray (shape) Distribution from which mean payoff for each profile is drawn. (default: lambda: 0) var_dist : (shape) -> ndarray (shape) Distribution from which payoff variance for each profile is drawn. (default: lambda: 1) covar_dist : (shape) -> ndarray (shape) Distribution from which the value of the off-diagonal covariance matrix entries for each profile is drawn. (default: uniform [-1, 1]) """ # Create sampling distributions and sample from them num_role_strats = list(num_role_strats) num_players = len(num_role_strats) shape = num_role_strats + [num_players] var = covar_dist(shape + [num_players]) diag = var.diagonal(0, num_players, num_players + 1) diag.setflags(write=True) # Hack np.copyto(diag, var_dist(shape)) # The next couple of lines do multivariate Gaussian sampling for all # payoffs simultaneously _, diag, right = np.linalg.svd(var) payoffs = rand.normal(size=shape) payoffs = np.einsum("...i,...i,...ij->...j", payoffs, np.sqrt(diag), right) payoffs += mean_dist(shape) return matgame.matgame(payoffs)
[docs]def two_player_zero_sum_game(num_role_strats, distribution=default_distribution): """Generate a two-player, zero-sum game""" # Generate player 1 payoffs num_role_strats = np.broadcast_to(num_role_strats, 2) p1_payoffs = distribution(num_role_strats)[..., None] return matgame.matgame(np.concatenate([p1_payoffs, -p1_payoffs], -1))
[docs]def sym_2p2s_game( a, b, c, d, distribution=default_distribution ): # pylint: disable=invalid-name """Create a symmetric 2-player 2-strategy game of the specified form. Four payoff values get drawn from U(min_val, max_val), and then are assigned to profiles in order from smallest to largest according to the order parameters as follows: +---+-----+-----+ | | s0 | s1 | +---+-----+-----+ |s0 | a,a | b,c | +---+-----+-----+ |s1 | c,b | d,d | +---+-----+-----+ distribution must accept a size parameter a la numpy distributions. """ utils.check({a, b, c, d} == set(range(4)), "numbers must be each of 1-4") # Generate payoffs payoffs = distribution(4) payoffs.sort() profs = [[2, 0], [1, 1], [0, 2]] pays = [[payoffs[a], 0], [payoffs[b], payoffs[c]], [0, payoffs[d]]] return paygame.game(2, 2, profs, pays)
[docs]def prisoners_dilemma(distribution=default_distribution): """Return a random prisoners dilemma game""" return sym_2p2s_game(2, 0, 3, 1, distribution)
[docs]def chicken(distribution=default_distribution): """Return a random prisoners dilemma game""" return sym_2p2s_game(0, 3, 1, 2, distribution)
[docs]def sym_2p2s_known_eq(eq_prob): """Generate a symmetric 2-player 2-strategy game This game has a single mixed equilibrium where strategy one is played with probability eq_prob. """ profiles = [[2, 0], [1, 1], [0, 2]] payoffs = [[0, 0], [eq_prob, 1 - eq_prob], [0, 0]] return paygame.game(2, 2, profiles, payoffs)
[docs]def polymatrix_game( num_players, num_strats, matrix_game=independent_game, players_per_matrix=2 ): """Creates a polymatrix game Each player's payoff in each profile is a sum over independent games played against each set of opponents. Each k-tuple of players plays an instance of the specified random k-player matrix game. Parameters ---------- num_players : int The number of players. num_strats : int The number of strategies per player. matrix_game : (players_per_matrix, num_strats) -> Game, optional A function to generate games between sub groups of players. players_per_matrix : int, optional The number of players that interact simultaneously. Notes ----- The actual roles and strategies of matrix game are ignored. """ payoffs = np.zeros([num_strats] * num_players + [num_players]) for players in itertools.combinations(range(num_players), players_per_matrix): sub_payoffs = matgame.matgame_copy( matrix_game([num_strats] * players_per_matrix) ).payoff_matrix() new_shape = np.array([1] * num_players + [players_per_matrix]) new_shape[list(players)] = num_strats payoffs[..., list(players)] += sub_payoffs.reshape(new_shape) return matgame.matgame(payoffs)
[docs]def rock_paper_scissors(win=1, loss=-1): """Return an instance of rock paper scissors""" if isinstance(win, abc.Iterable): win = list(win) else: win = [win] * 3 if isinstance(loss, abc.Iterable): loss = list(loss) else: loss = [loss] * 3 utils.check( all(l < 0 for l in loss) and all(w > 0 for w in win) and len(loss) == 3 and len(win) == 3, "win must be greater than 0 and loss must be less than zero", ) profiles = [[2, 0, 0], [1, 1, 0], [1, 0, 1], [0, 2, 0], [0, 1, 1], [0, 0, 2]] payoffs = [ [0.0, 0.0, 0.0], [loss[0], win[0], 0.0], [win[1], 0.0, loss[1]], [0.0, 0.0, 0.0], [0.0, loss[2], win[2]], [0.0, 0.0, 0.0], ] return paygame.game_names( ["all"], 2, [["paper", "rock", "scissors"]], profiles, payoffs )
[docs]def travellers_dilemma(players=2, max_value=100): """Return an instance of travellers dilemma Strategies range from 2 to max_value, thus there will be max_value - 1 strategies.""" utils.check(players > 1, "players must be more than one") utils.check(max_value > 2, "max value must be more than 2") base = rsgame.empty(players, max_value - 1) profiles = base.all_profiles() payoffs = np.zeros(profiles.shape) mins = np.argmax(profiles, -1) mask = profiles > 0 payoffs[mask] = mins.repeat(mask.sum(-1)) rows = np.arange(profiles.shape[0]) ties = profiles[rows, mins] > 1 lowest_pays = mins + 4 lowest_pays[ties] -= 2 payoffs[rows, mins] = lowest_pays return paygame.game_replace(base, profiles, payoffs)
[docs]def keep_profiles(base, keep_prob=0.5): """Keep random profiles from an existing game Parameters ---------- base : RsGame The game to take profiles from. keep_prob : float, optional The probability of keeping a profile from the full game. """ # First turn input into number of profiles to compute num_profs = base.num_profiles utils.check( 0 <= keep_prob <= 1, "keep probability must be in [0, 1] but was {:g}", keep_prob, ) if num_profs <= np.iinfo(int).max: num = rand.binomial(num_profs, keep_prob) else: num = round(float(num_profs * keep_prob)) return keep_num_profiles(base, num)
[docs]def keep_num_profiles(base, num): """Keep random profiles from an existing game Parameters ---------- base : RsGame Game to keep profiles from. num : int The number of profiles to keep from the game. """ utils.check( 0 <= num <= base.num_profiles, "num must be in [0, {:d}] but was {:d}", base.num_profiles, num, ) if num == 0: profiles = np.empty((0, base.num_strats), int) payoffs = np.empty((0, base.num_strats)) elif base.is_complete(): profiles = sample_profiles(base, num) payoffs = base.get_payoffs(profiles) else: inds = rand.choice(base.num_profiles, num, replace=False) profiles = base.profiles()[inds] payoffs = base.payoffs()[inds] return paygame.game_replace(base, profiles, payoffs)
[docs]def sample_profiles(base, num): # pylint: disable=inconsistent-return-statements """Generate unique profiles from a game Parameters ---------- base : RsGame Game to generate random profiles from. num : int Number of profiles to sample from the game. """ if num == base.num_all_profiles: # pylint: disable=no-else-return return base.all_profiles() elif num == 0: return np.empty((0, base.num_strats), int) elif base.num_all_profiles <= np.iinfo(int).max: inds = rand.choice(base.num_all_profiles, num, replace=False) return base.profile_from_id(inds) else: # Number of times we have to re-query ratio = sps.digamma(float(base.num_all_profiles)) - sps.digamma( float(base.num_all_profiles - num) ) # Max is for underflow num_per = max(round(float(ratio * base.num_all_profiles)), num) profiles = set() while len(profiles) < num: profiles.update(utils.hash_array(p) for p in base.random_profiles(num_per)) profiles = np.stack([h.array for h in profiles]) inds = rand.choice(profiles.shape[0], num, replace=False) return profiles[inds]
def _random_inputs(prob, num_strats, num_funcs): """Returns a random mask without all true or all false per function""" vals = np.random.random((num_strats, num_funcs)) mask = vals < prob inds = np.arange(num_funcs) mask[vals.argmin(0), inds] = True mask[vals.argmax(0), inds] = False return mask def _random_mask(prob, num_funcs, num_strats): """Returns a random mask with at least one true in every row and col""" vals = np.random.random((num_funcs, num_strats)) mask = vals < prob mask[np.arange(num_funcs), vals.argmin(1)] = True mask[vals.argmin(0), np.arange(num_strats)] = True return mask def _random_weights(prob, num_funcs, num_strats): """Returns random action weights""" return _random_mask(prob, num_funcs, num_strats) * np.random.normal( 0, 1, (num_funcs, num_strats) )
[docs]def normal_aggfn( role_players, role_strats, functions, *, input_prob=0.2, weight_prob=0.2 ): """Generate a random normal AgfnGame Each function value is an i.i.d Gaussian random walk. Parameters ---------- role_players : int or ndarray The number of players per role. role_strats : int or ndarray The number of strategies per role. functions : int The number of functions to generate. input_prob : float, optional The probability of a strategy counting towards a function value. weight_prob : float, optional The probability of a function producing non-zero payoffs to a strategy. """ base = rsgame.empty(role_players, role_strats) inputs = _random_inputs(input_prob, base.num_strats, functions) weights = _random_weights(weight_prob, functions, base.num_strats) shape = (functions,) + tuple(base.num_role_players + 1) funcs = np.random.normal(0, 1 / np.sqrt(base.num_players + 1), shape) for role in range(1, base.num_roles + 1): funcs.cumsum(role, out=funcs) mean = funcs.mean(tuple(range(1, base.num_roles + 1))) mean.shape = (functions,) + (1,) * base.num_roles funcs -= mean return aggfn.aggfn_replace(base, weights, inputs, funcs)
def _random_aggfn( # pylint: disable=too-many-arguments role_players, role_strats, functions, input_prob, weight_prob, role_dist ): """Base form for structured random aggfn generation role_dist takes a number of functions and a number of players and returns an ndarray of the function values. """ base = rsgame.empty(role_players, role_strats) inputs = _random_inputs(input_prob, base.num_strats, functions) weights = _random_weights(weight_prob, functions, base.num_strats) funcs = np.ones((functions,) + tuple(base.num_role_players + 1)) base_shape = [functions] + [1] * base.num_roles for role, play in enumerate(base.num_role_players): role_funcs = role_dist(functions, play) shape = base_shape.copy() shape[role + 1] = play + 1 role_funcs.shape = shape funcs *= role_funcs return aggfn.aggfn_replace(base, weights, inputs, funcs)
[docs]def poly_aggfn( role_players, role_strats, functions, *, input_prob=0.2, weight_prob=0.2, degree=4 ): """Generate a random polynomial AgfnGame Functions are generated by generating `degree` zeros in [0, num_players] to serve as a polynomial functions. Parameters ---------- role_players : int or ndarray The number of players per role. role_strats : int or ndarray The number of strategies per role. functions : int The number of functions to generate. input_prob : float, optional The probability of a strategy counting towards a function value. weight_prob : float, optional The probability of a function producing non-zero payoffs to a strategy. degree : int or [float], optional Either an integer specifying the degree or a list of the probabilities of degrees starting from one, e.g. 3 is the same as [0, 0, 1]. """ if isinstance(degree, int): degree = (0,) * (degree - 1) + (1,) max_degree = len(degree) def role_dist(functions, play): """Role distribution""" zeros = (np.random.random((functions, max_degree)) * 1.5 - 0.25) * play terms = np.arange(play + 1)[:, None] - zeros[:, None] choices = np.random.choice(max_degree, (functions, play + 1), True, degree) terms[choices[..., None] < np.arange(max_degree)] = 1 poly = terms.prod(2) / play ** choices # The prevents too many small polynomials from making functions # effectively constant scale = poly.max() - poly.min() offset = poly.min() + 1 return (poly - offset) / (1 if np.isclose(scale, 0) else scale) return _random_aggfn( role_players, role_strats, functions, input_prob, weight_prob, role_dist )
[docs]def sine_aggfn( role_players, role_strats, functions, *, input_prob=0.2, weight_prob=0.2, period=4 ): """Generate a random sinusodial AgfnGame Functions are generated by generating sinusoids with uniform random shifts and n periods in 0 to num_players, where n is chosen randomle between min_period and max_period. Parameters ---------- role_players : int or ndarray The number of players per role. role_strats : int or ndarray The number of strategies per role. functions : int The number of functions to generate. input_prob : float, optional The probability of a strategy counting towards a function value. weight_prob : float, optional The probability of a function producing non-zero payoffs to a strategy. period : float, optional The loose number of periods in the payoff for each function. """ def role_dist(functions, play): """Distribution by role""" # This setup makes it so that the beat frequencies approach period periods = ( (np.arange(1, functions + 1) + np.random.random(functions) / 2 - 1 / 4) * period / functions ) offset = np.random.random((functions, 1)) return np.sin( (np.linspace(0, 1, play + 1) * periods[:, None] + offset) * 2 * np.pi ) return _random_aggfn( role_players, role_strats, functions, input_prob, weight_prob, role_dist )
def _random_monotone_polynomial(functions, players, degree): """Generates a random monotone polynomial table""" coefs = np.random.random((functions, degree + 1)) / players ** np.arange(degree + 1) powers = np.arange(players + 1) ** np.arange(degree + 1)[:, None] return coefs.dot(powers)
[docs]def congestion(num_players, num_facilities, num_required, *, degree=2): """Generate a congestion game A congestion game is a symmetric game, where there are a given number of facilities, and each player must choose to use some amount of them. The payoff for each facility decreases as more players use it, and a players utility is the sum of the utilities for every facility. In this formulation, facility payoffs are random polynomials of the number of people using said facility. Parameters ---------- num_players : int > 1 The number of players. num_facilities : int > 1 The number of facilities. num_required : 0 < int < num_facilities The number of required facilities. degree : int > 0, optional Degree of payoff polynomials. """ utils.check(num_players > 1, "must have more than one player") utils.check(num_facilities > 1, "must have more than one facility") utils.check( 0 < num_required < num_facilities, "must require more than zero but less than num_facilities", ) utils.check(degree > 0, "degree must be greater than zero") function_inputs = utils.acomb(num_facilities, num_required) functions = -_random_monotone_polynomial(num_facilities, num_players, degree) facs = tuple(utils.prefix_strings("", num_facilities)) strats = tuple( "_".join(facs[i] for i, m in enumerate(mask) if m) for mask in function_inputs ) return aggfn.aggfn_names( ["all"], num_players, [strats], function_inputs.T, function_inputs, functions )
[docs]def local_effect(num_players, num_strategies, *, edge_prob=0.2): """Generate a local effect game In a local effect game, strategies are connected by a graph, and utilities are a function of the number of players playing our strategy and the number of players playing a neighboring strategy, hence local effect. In this formulation, payoffs for others playing our strategy are negative quadratics, and payoffs for playing other strategies are positive cubics. Parameters ---------- num_players : int > 1 The number of players. num_strategies : int > 1 The number of strategies. edge_prob : float, optional The probability that one strategy affects another. """ utils.check(num_players > 1, "can't generate a single player game") utils.check(num_strategies > 1, "can't generate a single strategy game") local_effect_graph = np.random.rand(num_strategies, num_strategies) < edge_prob np.fill_diagonal(local_effect_graph, False) num_neighbors = local_effect_graph.sum() num_functions = num_neighbors + num_strategies action_weights = np.eye(num_functions, num_strategies, dtype=float) function_inputs = np.eye(num_strategies, num_functions, dtype=bool) in_act, out_act = local_effect_graph.nonzero() func_inds = np.arange(num_strategies, num_functions) function_inputs[in_act, func_inds] = True action_weights[func_inds, out_act] = 1 function_table = np.empty((num_functions, num_players + 1), float) function_table[:num_strategies] = -_random_monotone_polynomial( num_strategies, num_players, 2 ) function_table[num_strategies:] = _random_monotone_polynomial( num_neighbors, num_players, 3 ) return aggfn.aggfn( num_players, num_strategies, action_weights, function_inputs, function_table )