Source code for axelrod.strategies.meta
import numpy as np
from axelrod.action import Action
from axelrod.classifier import Classifiers
from axelrod.player import Player
from axelrod.strategies import TitForTat
from axelrod.strategy_transformers import NiceTransformer
from ._strategies import all_strategies
from .hunter import (
AlternatorHunter,
CooperatorHunter,
CycleHunter,
DefectorHunter,
EventualCycleHunter,
MathConstantHunter,
RandomHunter,
)
# Needs to be computed manually to prevent circular dependency
ordinary_strategies = [
s for s in all_strategies if Classifiers.obey_axelrod(s())
]
C, D = Action.C, Action.D
[docs]class MetaPlayer(Player):
"""
A generic player that has its own team of players.
Names:
- Meta Player: Original name by Karol Langner
"""
name = "Meta Player"
classifier = {
"memory_depth": float("inf"), # Long memory
"stochastic": True,
"long_run_time": True,
"inspects_source": False,
"manipulates_source": False,
"manipulates_state": False,
}
def __init__(self, team=None):
# The default is to use all strategies available, but we need to import
# the list at runtime, since _strategies import also _this_ module
# before defining the list.
if team:
self.team = team
else:
self.team = ordinary_strategies
# Make sure we don't use any meta players to avoid infinite recursion.
self.team = [t for t in self.team if not issubclass(t, MetaPlayer)]
# Initiate all the players in our team.
self.team = [t() for t in self.team]
self._last_results = None
super().__init__()
def _post_init(self):
# The player's classification characteristics are derived from the team.
# Note that memory_depth is not simply the max memory_depth of the team.
for key in [
"stochastic",
"inspects_source",
"manipulates_source",
"manipulates_state",
]:
self.classifier[key] = any(map(Classifiers[key], self.team))
self.classifier["makes_use_of"] = set()
for t in self.team:
new_uses = Classifiers["makes_use_of"](t)
if new_uses:
self.classifier["makes_use_of"].update(new_uses)
[docs] def set_seed(self, seed=None):
super().set_seed(seed=seed)
# Seed the team as well
for t in self.team:
t.set_seed(self._random.random_seed_int())
def receive_match_attributes(self):
for t in self.team:
t.set_match_attributes(**self.match_attributes)
def __repr__(self):
team_size = len(self.team)
return "{}: {} player{}".format(
self.name, team_size, "s" if team_size > 1 else ""
)
def update_histories(self, coplay):
# Update team histories.
try:
for player, play in zip(self.team, self._last_results):
player.update_history(play, coplay)
except TypeError:
# If the Meta class is decorated by the Joss-Ann transformer,
# such that the decorated class is now deterministic, the underlying
# strategy isn't called. In that case, updating the history of all the
# team members doesn't matter.
# As a sanity check, look for at least one reclassifier, otherwise
# this try-except clause could hide a bug.
if len(self._reclassifiers) == 0:
raise TypeError(
"MetaClass update_histories issue, expected a reclassifier."
)
# Otherwise just update with C always, so at least the histories have the
# expected length.
for player in self.team:
player.update_history(C, coplay)
def update_history(self, play, coplay):
super().update_history(play, coplay)
self.update_histories(coplay)
[docs] def strategy(self, opponent):
"""Actual strategy definition that determines player's action."""
# Get the results of all our players.
results = []
for player in self.team:
play = player.strategy(opponent)
results.append(play)
self._last_results = results
# A subclass should just define a way to choose the result based on
# team results.
return self.meta_strategy(results, opponent)
[docs] def meta_strategy(self, results, opponent):
"""Determine the meta result based on results of all players.
Override this function in child classes."""
return C
[docs]class MetaMajority(MetaPlayer):
"""A player who goes by the majority vote of all other non-meta players.
Names:
- Meta Majority: Original name by Karol Langner
"""
name = "Meta Majority"
[docs] @staticmethod
def meta_strategy(results, opponent):
if results.count(D) > results.count(C):
return D
return C
[docs]class MetaMinority(MetaPlayer):
"""A player who goes by the minority vote of all other non-meta players.
Names:
- Meta Minority: Original name by Karol Langner
"""
name = "Meta Minority"
[docs] @staticmethod
def meta_strategy(results, opponent):
if results.count(D) < results.count(C):
return D
return C
[docs]class MetaWinner(MetaPlayer):
"""A player who goes by the strategy of the current winner.
Names:
- Meta Winner: Original name by Karol Langner
"""
name = "Meta Winner"
def __init__(self, team=None):
super().__init__(team=team)
# For each player, we will keep the history of proposed moves and
# a running score since the beginning of the game.
self.scores = np.zeros(len(self.team))
self.classifier["long_run_time"] = True
def _update_scores(self, coplay):
# Update the running score for each player, before determining the
# next move.
game = self.match_attributes["game"]
scores = []
for player in self.team:
last_round = (player.history[-1], coplay)
s = game.scores[last_round][0]
scores.append(s)
self.scores += np.array(scores)
def update_histories(self, coplay):
super().update_histories(coplay)
self._update_scores(coplay)
[docs] def meta_strategy(self, results, opponent):
# Choose an action based on the collection of scores
bestscore = max(self.scores)
beststrategies = [
i for (i, score) in enumerate(self.scores) if score == bestscore
]
bestproposals = [results[i] for i in beststrategies]
bestresult = C if C in bestproposals else D
return bestresult
NiceMetaWinner = NiceTransformer()(MetaWinner)
[docs]class MetaWinnerEnsemble(MetaWinner):
"""A variant of MetaWinner that chooses one of the top scoring strategies
at random against each opponent. Note this strategy is always stochastic
regardless of the team, if team larger than 1, and the players are distinct.
Names:
- Meta Winner Ensemble: Original name by Marc Harper
"""
name = "Meta Winner Ensemble"
def _post_init(self):
super()._post_init()
team = list(t.__class__ for t in self.team)
if len(team) > 1:
self.classifier["stochastic"] = True
self.singular = False
else:
self.singular = True
# If the team has repeated identical members, then it reduces to a singular team
# and it may not actually be stochastic.
if team and len(set(team)) == 1:
self.classifier["stochastic"] = Classifiers["stochastic"](
self.team[0]
)
self.singular = True
[docs] def meta_strategy(self, results, opponent):
# If the team consists of identical players, just take the first result.
# This prevents an unnecessary call to _random below.
if self.singular:
return results[0]
# Sort by score
scores = [(score, i) for (i, score) in enumerate(self.scores)]
# Choose one of the best scorers at random
scores.sort(reverse=True)
prop = max(1, int(len(scores) * 0.08))
best_scorers = [i for (s, i) in scores[:prop]]
index = self._random.choice(best_scorers)
return results[index]
NiceMetaWinnerEnsemble = NiceTransformer()(MetaWinnerEnsemble)
[docs]class MetaHunter(MetaPlayer):
"""A player who uses a selection of hunters.
Names
- Meta Hunter: Original name by Karol Langner
"""
name = "Meta Hunter"
classifier = {
"memory_depth": float("inf"), # Long memory
"stochastic": False,
"long_run_time": False,
"inspects_source": False,
"manipulates_source": False,
"manipulates_state": False,
}
def __init__(self):
# Notice that we don't include the cooperator hunter, because it leads
# to excessive defection and therefore bad performance against
# unforgiving strategies. We will stick to hunters that use defections
# as cues. However, a really tangible benefit comes from combining
# Random Hunter and Math Constant Hunter, since together they catch
# strategies that are lightly randomized but still quite constant
# (the tricky/suspicious ones).
team = [
DefectorHunter,
AlternatorHunter,
RandomHunter,
MathConstantHunter,
CycleHunter,
EventualCycleHunter,
]
super().__init__(team=team)
[docs] @staticmethod
def meta_strategy(results, opponent):
# If any of the hunters smells prey, then defect!
if D in results:
return D
# Tit-for-tat might seem like a better default choice, but in many
# cases it complicates the heuristics of hunting and creates
# false-positives. So go ahead and use it, but only for longer
# histories.
if len(opponent.history) > 100:
return D if opponent.history[-1:] == [D] else C
else:
return C
[docs]class MetaHunterAggressive(MetaPlayer):
"""A player who uses a selection of hunters.
Names
- Meta Hunter Aggressive: Original name by Marc Harper
"""
name = "Meta Hunter Aggressive"
classifier = {
"memory_depth": float("inf"), # Long memory
"stochastic": False,
"long_run_time": False,
"inspects_source": False,
"manipulates_source": False,
"manipulates_state": False,
}
def __init__(self, team=None):
# This version uses CooperatorHunter
if team is None:
team = [
DefectorHunter,
AlternatorHunter,
RandomHunter,
MathConstantHunter,
CycleHunter,
EventualCycleHunter,
CooperatorHunter,
]
super().__init__(team=team)
[docs] @staticmethod
def meta_strategy(results, opponent):
# If any of the hunters smells prey, then defect!
if D in results:
return D
# Tit-for-tat might seem like a better default choice, but in many
# cases it complicates the heuristics of hunting and creates
# false-positives. So go ahead and use it, but only for longer
# histories.
if len(opponent.history) > 100:
return D if opponent.history[-1:] == [D] else C
else:
return C
[docs]class MetaMajorityMemoryOne(MetaMajority):
"""MetaMajority with the team of Memory One players
Names
- Meta Majority Memory One: Original name by Marc Harper
"""
name = "Meta Majority Memory One"
def __init__(self):
team = [
s
for s in ordinary_strategies
if Classifiers["memory_depth"](s()) <= 1
]
super().__init__(team=team)
self.classifier["long_run_time"] = False
[docs]class MetaMajorityFiniteMemory(MetaMajority):
"""MetaMajority with the team of Finite Memory Players
Names
- Meta Majority Finite Memory: Original name by Marc Harper
"""
name = "Meta Majority Finite Memory"
def __init__(self):
team = [
s
for s in ordinary_strategies
if Classifiers["memory_depth"](s()) < float("inf")
]
super().__init__(team=team)
[docs]class MetaMajorityLongMemory(MetaMajority):
"""MetaMajority with the team of Long (infinite) Memory Players
Names
- Meta Majority Long Memory: Original name by Marc Harper
"""
name = "Meta Majority Long Memory"
def __init__(self):
team = [
s
for s in ordinary_strategies
if Classifiers["memory_depth"](s()) == float("inf")
]
super().__init__(team=team)
[docs]class MetaWinnerMemoryOne(MetaWinner):
"""MetaWinner with the team of Memory One players
Names
- Meta Winner Memory Memory One: Original name by Marc Harper
"""
name = "Meta Winner Memory One"
def __init__(self):
team = [
s
for s in ordinary_strategies
if Classifiers["memory_depth"](s()) <= 1
]
super().__init__(team=team)
self.classifier["long_run_time"] = False
[docs]class MetaWinnerFiniteMemory(MetaWinner):
"""MetaWinner with the team of Finite Memory Players
Names
- Meta Winner Finite Memory: Original name by Marc Harper
"""
name = "Meta Winner Finite Memory"
def __init__(self):
team = [
s
for s in ordinary_strategies
if Classifiers["memory_depth"](s()) < float("inf")
]
super().__init__(team=team)
[docs]class MetaWinnerLongMemory(MetaWinner):
"""MetaWinner with the team of Long (infinite) Memory Players
Names
- Meta Winner Long Memory: Original name by Marc Harper
"""
name = "Meta Winner Long Memory"
def __init__(self):
team = [
s
for s in ordinary_strategies
if Classifiers["memory_depth"](s()) == float("inf")
]
super().__init__(team=team)
[docs]class MetaWinnerDeterministic(MetaWinner):
"""Meta Winner with the team of Deterministic Players.
Names
- Meta Winner Deterministic: Original name by Marc Harper
"""
name = "Meta Winner Deterministic"
def __init__(self):
team = [
s for s in ordinary_strategies if not Classifiers["stochastic"](s())
]
super().__init__(team=team)
self.classifier["stochastic"] = False
[docs]class MetaWinnerStochastic(MetaWinner):
"""Meta Winner with the team of Stochastic Players.
Names
- Meta Winner Stochastic: Original name by Marc Harper
"""
name = "Meta Winner Stochastic"
def __init__(self):
team = [
s for s in ordinary_strategies if Classifiers["stochastic"](s())
]
super().__init__(team=team)
[docs]class MetaMixer(MetaPlayer):
"""A player who randomly switches between a team of players.
If no distribution is passed then the player will uniformly choose between
sub players.
In essence this is creating a Mixed strategy.
Parameters
team : list of strategy classes, optional
Team of strategies that are to be randomly played
If none is passed will select the ordinary strategies.
distribution : list representing a probability distribution, optional
This gives the distribution from which to select the players.
If none is passed will select uniformly.
Names
- Meta Mixer: Original name by Vince Knight
"""
name = "Meta Mixer"
classifier = {
"memory_depth": float("inf"), # Long memory
"stochastic": True,
"long_run_time": True,
"inspects_source": False,
"manipulates_source": False,
"manipulates_state": False,
}
def __init__(self, team=None, distribution=None):
super().__init__(team=team)
# Check that distribution is not all zeros, which will make numpy unhappy.
if distribution and all(x == 0 for x in distribution):
distribution = None
self.distribution = distribution
def _post_init(self):
distribution = self.distribution
if distribution and len(set(distribution)) > 1:
self.classifier["stochastic"] = True
if len(self.team) == 1:
self.classifier["stochastic"] = Classifiers["stochastic"](
self.team[0]
)
# Overwrite strategy to avoid use of _random. This will ignore self.meta_strategy.
self.index = 0
self.strategy = self.index_strategy
return
# Check if the distribution has only one non-zero value. If so, the strategy may be
# deterministic, and we can avoid _random.
if distribution:
total = sum(distribution)
distribution = np.array(distribution) / total
if 1 in distribution:
self.index = list(distribution).index(1)
# It's potentially deterministic.
self.classifier["stochastic"] = Classifiers["stochastic"](
self.team[self.index]
)
# Overwrite strategy to avoid use of _random. This will ignore self.meta_strategy.
self.strategy = self.index_strategy
[docs] def index_strategy(self, opponent):
"""When the team effectively has a single player, only use that strategy."""
results = [C] * len(self.team)
player = self.team[self.index]
action = player.strategy(opponent)
results[self.index] = action
self._last_results = results
return action
[docs] def meta_strategy(self, results, opponent):
"""Using the _random.choice function to sample with weights."""
return self._random.choice(results, p=self.distribution)
[docs]class NMWEDeterministic(NiceMetaWinnerEnsemble):
"""Nice Meta Winner Ensemble with the team of Deterministic Players.
Names
- Nice Meta Winner Ensemble Deterministic: Original name by Marc Harper
"""
name = "NMWE Deterministic"
def __init__(self):
team = [
s for s in ordinary_strategies if not Classifiers["stochastic"](s())
]
super().__init__(team=team)
self.classifier["stochastic"] = True
[docs]class NMWEStochastic(NiceMetaWinnerEnsemble):
"""Nice Meta Winner Ensemble with the team of Stochastic Players.
Names
- Nice Meta Winner Ensemble Stochastic: Original name by Marc Harper
"""
name = "NMWE Stochastic"
def __init__(self):
team = [
s for s in ordinary_strategies if Classifiers["stochastic"](s())
]
super().__init__(team=team)
[docs]class NMWEFiniteMemory(NiceMetaWinnerEnsemble):
"""Nice Meta Winner Ensemble with the team of Finite Memory Players.
Names
- Nice Meta Winner Ensemble Finite Memory: Original name by Marc Harper
"""
name = "NMWE Finite Memory"
def __init__(self):
team = [
s
for s in ordinary_strategies
if Classifiers["memory_depth"](s()) < float("inf")
]
super().__init__(team=team)
[docs]class NMWELongMemory(NiceMetaWinnerEnsemble):
"""Nice Meta Winner Ensemble with the team of Long Memory Players.
Names
- Nice Meta Winner Ensemble Long Memory: Original name by Marc Harper
"""
name = "NMWE Long Memory"
def __init__(self):
team = [
s
for s in ordinary_strategies
if Classifiers["memory_depth"](s()) == float("inf")
]
super().__init__(team=team)
[docs]class NMWEMemoryOne(NiceMetaWinnerEnsemble):
"""Nice Meta Winner Ensemble with the team of Memory One Players.
Names
- Nice Meta Winner Ensemble Memory One: Original name by Marc Harper
"""
name = "NMWE Memory One"
def __init__(self):
team = [
s
for s in ordinary_strategies
if Classifiers["memory_depth"](s()) <= 1
]
super().__init__(team=team)
self.classifier["long_run_time"] = False
[docs]class MemoryDecay(MetaPlayer):
"""
A player utilizes the (default) Tit for Tat strategy for the first (default) 15 turns,
at the same time memorizing the opponent's decisions. After the 15 turns have
passed, the player calculates a 'net cooperation score' (NCS) for their opponent,
weighing decisions to Cooperate as (default) 1, and to Defect as (default)
-2. If the opponent's NCS is below 0, the player defects; otherwise,
they cooperate.
The player's memories of the opponent's decisions have a random chance to be
altered (i.e., a C decision becomes D or vice versa; default probability
is 0.03) or deleted (default probability is 0.1).
It is possible to pass a different axelrod player class to change the initial
player behavior.
Name: Memory Decay
"""
name = "Memory Decay"
classifier = {
"memory_depth": float("inf"),
"long_run_time": False,
"stochastic": True,
"inspects_source": False,
"manipulates_source": False,
"manipulates_state": False,
}
def __init__(
self,
p_memory_delete: float = 0.1,
p_memory_alter: float = 0.03,
loss_value: float = -2,
gain_value: float = 1,
memory: list = None,
start_strategy: Player = TitForTat,
start_strategy_duration: int = 15,
):
super().__init__(team=[start_strategy])
self.p_memory_delete = p_memory_delete
self.p_memory_alter = p_memory_alter
self.loss_value = loss_value
self.gain_value = gain_value
self.memory = [] if not memory else memory
self.start_strategy_duration = start_strategy_duration
self.gloss_values = None
def _post_init(self):
super()._post_init()
# This strategy is stochastic even if none of the team is. The
# MetaPlayer initializer will set stochastic to be False in that case.
self.classifier["stochastic"] = True
def __repr__(self):
return Player.__repr__(self)
[docs] def gain_loss_translate(self):
"""
Translates the actions (D and C) to numeric values (loss_value and
gain_value).
"""
values = {C: self.gain_value, D: self.loss_value}
self.gloss_values = [values[action] for action in self.memory]
[docs] def memory_alter(self):
"""
Alters memory entry, i.e. puts C if there's a D and vice versa.
"""
alter = self._random.choice(range(0, len(self.memory)))
self.memory[alter] = self.memory[alter].flip()
[docs] def memory_delete(self):
"""
Deletes memory entry.
"""
self.memory.pop(self._random.choice(range(0, len(self.memory))))
[docs] def meta_strategy(self, results, opponent):
try:
self.memory.append(opponent.history[-1])
except IndexError:
pass
if len(self.history) < self.start_strategy_duration:
return results[0]
else:
if self._random.random() <= self.p_memory_alter:
self.memory_alter()
if self._random.random() <= self.p_memory_delete:
self.memory_delete()
self.gain_loss_translate()
if sum(self.gloss_values) < 0:
return D
else:
return C