Source code for axelrod.strategies.lookerup

from collections import namedtuple
from itertools import product
from typing import Any, TypeVar

from axelrod.action import Action, actions_to_str, str_to_actions
from axelrod.evolvable_player import (
    EvolvablePlayer,
    InsufficientParametersError,
    crossover_dictionaries,
)
from axelrod.player import Player

C, D = Action.C, Action.D
actions = (C, D)

Plays = namedtuple("Plays", "self_plays, op_plays, op_openings")
Reaction = TypeVar("Reaction", Action, float)


[docs]class LookupTable(object): """ LookerUp and its children use this object to determine their next actions. It is an object that creates a table of all possible plays to a specified depth and the action to be returned for each combination of plays. The "get" method returns the appropriate response. For the table containing:: .... Plays(self_plays=(C, C), op_plays=(C, D), op_openings=(D, C): D Plays(self_plays=(C, C), op_plays=(C, D), op_openings=(D, D): C ... with: player.history[-2:]=[C, C] and opponent.history[-2:]=[C, D] and opponent.history[:2]=[D, D], calling LookupTable.get(plays=(C, C), op_plays=(C, D), op_openings=(D, D)) will return C. Instantiate the table with a lookup_dict. This is {(self_plays_tuple, op_plays_tuple, op_openings_tuple): action, ...}. It must contain every possible permutation with C's and D's of the above tuple. so:: good_dict = {((C,), (C,), ()): C, ((C,), (D,), ()): C, ((D,), (C,), ()): D, ((D,), (D,), ()): C} bad_dict = {((C,), (C,), ()): C, ((C,), (D,), ()): C, ((D,), (C,), ()): D} LookupTable.from_pattern() creates an ordered list of keys for you and maps the pattern to the keys.:: LookupTable.from_pattern(pattern=(C, D, D, C), player_depth=0, op_depth=1, op_openings_depth=1 ) creates the dictionary:: {Plays(self_plays=(), op_plays=(C), op_openings=(C)): C, Plays(self_plays=(), op_plays=(C), op_openings=(D)): D, Plays(self_plays=(), op_plays=(D), op_openings=(C)): D, Plays(self_plays=(), op_plays=(D), op_openings=(D)): C,} and then returns a LookupTable with that dictionary. """ def __init__(self, lookup_dict: dict) -> None: self._dict = make_keys_into_plays(lookup_dict) sample_key = next(iter(self._dict)) self._plays_depth = len(sample_key.self_plays) self._op_plays_depth = len(sample_key.op_plays) self._op_openings_depth = len(sample_key.op_openings) self._table_depth = max( self._plays_depth, self._op_plays_depth, self._op_openings_depth ) self._raise_error_for_bad_lookup_dict() def _raise_error_for_bad_lookup_dict(self): if any( len(key.self_plays) != self._plays_depth or len(key.op_plays) != self._op_plays_depth or len(key.op_openings) != self._op_openings_depth for key in self._dict ): raise ValueError("Lookup table keys are not all the same size.") total_key_combinations = 2 ** ( self._plays_depth + self._op_plays_depth + self._op_openings_depth ) if total_key_combinations != len(self._dict): msg = ( "Lookup table does not have enough keys" + " to cover all possibilities." ) raise ValueError(msg) @classmethod def from_pattern( cls, pattern: tuple, player_depth: int, op_depth: int, op_openings_depth: int, ): keys = create_lookup_table_keys( player_depth=player_depth, op_depth=op_depth, op_openings_depth=op_openings_depth, ) if len(keys) != len(pattern): msg = "Pattern must be len: {}, but was len: {}".format( len(keys), len(pattern) ) raise ValueError(msg) input_dict = dict(zip(keys, pattern)) return cls(input_dict) def get(self, plays: tuple, op_plays: tuple, op_openings: tuple) -> Any: return self._dict[ Plays(self_plays=plays, op_plays=op_plays, op_openings=op_openings) ] @property def player_depth(self) -> int: return self._plays_depth @property def op_depth(self) -> int: return self._op_plays_depth @property def op_openings_depth(self) -> int: return self._op_openings_depth @property def table_depth(self) -> int: return self._table_depth @property def dictionary(self) -> dict: return self._dict.copy()
[docs] def display( self, sort_by: tuple = ("op_openings", "self_plays", "op_plays") ) -> str: """ Returns a string for printing lookup_table info in specified order. :param sort_by: only_elements='self_plays', 'op_plays', 'op_openings' """ def sorter(plays): return tuple( actions_to_str(getattr(plays, field) for field in sort_by) ) col_width = 11 sorted_keys = sorted(self._dict, key=sorter) header_line = ( "{str_list[0]:^{width}}|" + "{str_list[1]:^{width}}|" + "{str_list[2]:^{width}}" ) display_line = header_line.replace("|", ",") + ": {str_list[3]}," def make_commaed_str(action_tuple): return ", ".join(str(action) for action in action_tuple) line_elements = [ ( make_commaed_str(getattr(key, sort_by[0])), make_commaed_str(getattr(key, sort_by[1])), make_commaed_str(getattr(key, sort_by[2])), self._dict[key], ) for key in sorted_keys ] header = header_line.format(str_list=sort_by, width=col_width) + "\n" lines = [ display_line.format(str_list=line, width=col_width) for line in line_elements ] return header + "\n".join(lines) + "\n"
def __eq__(self, other) -> bool: if not isinstance(other, LookupTable): return False return self._dict == other.dictionary
[docs]def make_keys_into_plays(lookup_table: dict) -> dict: """Returns a dict where all keys are Plays.""" new_table = lookup_table.copy() if any(not isinstance(key, Plays) for key in new_table): new_table = {Plays(*key): value for key, value in new_table.items()} return new_table
[docs]def create_lookup_table_keys( player_depth: int, op_depth: int, op_openings_depth: int ) -> list: """Returns a list of Plays that has all possible permutations of C's and D's for each specified depth. the list is in order, C < D sorted by ((player_tuple), (op_tuple), (op_openings_tuple)). create_lookup_keys(2, 1, 0) returns:: [Plays(self_plays=(C, C), op_plays=(C,), op_openings=()), Plays(self_plays=(C, C), op_plays=(D,), op_openings=()), Plays(self_plays=(C, D), op_plays=(C,), op_openings=()), Plays(self_plays=(C, D), op_plays=(D,), op_openings=()), Plays(self_plays=(D, C), op_plays=(C,), op_openings=()), Plays(self_plays=(D, C), op_plays=(D,), op_openings=()), Plays(self_plays=(D, D), op_plays=(C,), op_openings=()), Plays(self_plays=(D, D), op_plays=(D,), op_openings=())] """ self_plays = product((C, D), repeat=player_depth) op_plays = product((C, D), repeat=op_depth) op_openings = product((C, D), repeat=op_openings_depth) iterator = product(self_plays, op_plays, op_openings) return [Plays(*plays_tuple) for plays_tuple in iterator]
default_tft_lookup_table = { Plays(self_plays=(), op_plays=(D,), op_openings=()): D, Plays(self_plays=(), op_plays=(C,), op_openings=()): C, }
[docs]class LookerUp(Player): """ This strategy uses a LookupTable to decide its next action. If there is not enough history to use the table, it calls from a list of self.initial_actions. if self_depth=2, op_depth=3, op_openings_depth=5, LookerUp finds the last 2 plays of self, the last 3 plays of opponent and the opening 5 plays of opponent. It then looks those up on the LookupTable and returns the appropriate action. If 5 rounds have not been played (the minimum required for op_openings_depth), it calls from self.initial_actions. LookerUp can be instantiated with a dictionary. The dictionary uses tuple(tuple, tuple, tuple) or Plays as keys. for example. - self_plays: depth=2 - op_plays: depth=1 - op_openings: depth=0:: {Plays((C, C), (C), ()): C, Plays((C, C), (D), ()): D, Plays((C, D), (C), ()): D, <- example below Plays((C, D), (D), ()): D, Plays((D, C), (C), ()): C, Plays((D, C), (D), ()): D, Plays((D, D), (C), ()): C, Plays((D, D), (D), ()): D} From the above table, if the player last played C, D and the opponent last played C (here the initial opponent play is ignored) then this round, the player would play D. The dictionary must contain all possible permutations of C's and D's. LookerUp can also be instantiated with `pattern=str/tuple` of actions, and:: parameters=Plays( self_plays=player_depth: int, op_plays=op_depth: int, op_openings=op_openings_depth: int) It will create keys of len=2 ** (sum(parameters)) and map the pattern to the keys. initial_actions is a tuple such as (C, C, D). A table needs initial actions equal to max(self_plays depth, opponent_plays depth, opponent_initial_plays depth). If provided initial_actions is too long, the extra will be ignored. If provided initial_actions is too short, the shortfall will be made up with C's. Some well-known strategies can be expressed as special cases; for example Cooperator is given by the dict (All history is ignored and always play C):: {Plays((), (), ()) : C} Tit-For-Tat is given by (The only history that is important is the opponent's last play.):: {Plays((), (D,), ()): D, Plays((), (C,), ()): C} LookerUp's LookupTable defaults to Tit-For-Tat. The initial_actions defaults to playing C. Names: - Lookerup: Original name by Martin Jones """ name = "LookerUp" classifier = { "memory_depth": float("inf"), "stochastic": False, "long_run_time": False, "inspects_source": False, "manipulates_source": False, "manipulates_state": False, } default_tft_lookup_table = { Plays(self_plays=(), op_plays=(D,), op_openings=()): D, Plays(self_plays=(), op_plays=(C,), op_openings=()): C, } def __init__( self, lookup_dict: dict = None, initial_actions: tuple = None, pattern: Any = None, # pattern is str or tuple of Action's. parameters: Plays = None, ) -> None: Player.__init__(self) self.parameters = parameters self.pattern = pattern self._lookup = self._get_lookup_table(lookup_dict, pattern, parameters) self._set_memory_depth() self.initial_actions = self._get_initial_actions(initial_actions) self._initial_actions_pool = list(self.initial_actions) @classmethod def _get_lookup_table( cls, lookup_dict: dict, pattern: Any, parameters: tuple ) -> LookupTable: if lookup_dict: return LookupTable(lookup_dict=lookup_dict) if pattern is not None and parameters is not None: if isinstance(pattern, str): pattern = str_to_actions(pattern) self_depth, op_depth, op_openings_depth = parameters return LookupTable.from_pattern( pattern, self_depth, op_depth, op_openings_depth ) return LookupTable(default_tft_lookup_table) def _set_memory_depth(self): if self._lookup.op_openings_depth == 0: self.classifier["memory_depth"] = self._lookup.table_depth else: self.classifier["memory_depth"] = float("inf") def _get_initial_actions(self, initial_actions: tuple) -> tuple: """Initial actions will always be cut down to table_depth.""" table_depth = self._lookup.table_depth if not initial_actions: return tuple([C] * table_depth) initial_actions_shortfall = table_depth - len(initial_actions) if initial_actions_shortfall > 0: return initial_actions + tuple([C] * initial_actions_shortfall) return initial_actions[:table_depth]
[docs] def strategy(self, opponent: Player) -> Reaction: turn_index = len(opponent.history) while turn_index < len(self._initial_actions_pool): return self._initial_actions_pool[turn_index] player_last_n_plays = get_last_n_plays( player=self, depth=self._lookup.player_depth ) opponent_last_n_plays = get_last_n_plays( player=opponent, depth=self._lookup.op_depth ) opponent_initial_plays = tuple( opponent.history[: self._lookup.op_openings_depth] ) return self._lookup.get( player_last_n_plays, opponent_last_n_plays, opponent_initial_plays )
@property def lookup_dict(self): return self._lookup.dictionary
[docs] def lookup_table_display( self, sort_by: tuple = ("op_openings", "self_plays", "op_plays") ) -> str: """ Returns a string for printing lookup_table info in specified order. :param sort_by: only_elements='self_plays', 'op_plays', 'op_openings' """ return self._lookup.display(sort_by=sort_by)
[docs]class EvolvableLookerUp(LookerUp, EvolvablePlayer): name = "EvolvableLookerUp" def __init__( self, lookup_dict: dict = None, initial_actions: tuple = None, pattern: Any = None, # pattern is str or tuple of Action's. parameters: Plays = None, mutation_probability: float = None, seed: int = None, ) -> None: EvolvablePlayer.__init__(self, seed=seed) ( lookup_dict, initial_actions, pattern, parameters, mutation_probability, ) = self._normalize_parameters( lookup_dict, initial_actions, pattern, parameters, mutation_probability, ) LookerUp.__init__( self, lookup_dict=lookup_dict, initial_actions=initial_actions, pattern=pattern, parameters=parameters, ) self.mutation_probability = mutation_probability self.overwrite_init_kwargs( lookup_dict=lookup_dict, initial_actions=initial_actions, pattern=pattern, parameters=parameters, mutation_probability=mutation_probability, ) def _normalize_parameters( self, lookup_dict=None, initial_actions=None, pattern=None, parameters=None, mutation_probability=None, ): if lookup_dict and initial_actions: # Compute the associated pattern and parameters # Map the table keys to namedTuple Plays lookup_table = self._get_lookup_table( lookup_dict, pattern, parameters ) lookup_dict = lookup_table.dictionary parameters = ( lookup_table.player_depth, lookup_table.op_depth, lookup_table.op_openings_depth, ) pattern = tuple(v for k, v in sorted(lookup_dict.items())) elif pattern and parameters and initial_actions: # Compute the associated lookup table lookup_table = self._get_lookup_table( lookup_dict, pattern, parameters ) plays, op_plays, op_start_plays = parameters lookup_table = self._get_lookup_table( lookup_dict, pattern, parameters ) lookup_dict = lookup_table.dictionary elif parameters: # Generate a random pattern and (maybe) initial actions plays, op_plays, op_start_plays = parameters pattern, lookup_table = self.random_params( plays, op_plays, op_start_plays ) lookup_dict = lookup_table.dictionary if not initial_actions: num_actions = max([plays, op_plays, op_start_plays]) initial_actions = tuple( [self._random.choice((C, D)) for _ in range(num_actions)] ) else: raise InsufficientParametersError( "Insufficient Parameters to instantiate EvolvableLookerUp" ) # Normalize pattern if isinstance(pattern, str): pattern = str_to_actions(pattern) pattern = tuple(pattern) if mutation_probability is None: plays, op_plays, op_start_plays = parameters keys = create_lookup_table_keys(plays, op_plays, op_start_plays) mutation_probability = 2.0 / len(keys) return ( lookup_dict, initial_actions, pattern, parameters, mutation_probability, ) def random_value(self): return self._random.choice(actions) def random_params(self, plays, op_plays, op_start_plays): keys = create_lookup_table_keys(plays, op_plays, op_start_plays) # To get a pattern, we just randomly pick between C and D for each key pattern = [self.random_value() for _ in keys] table = dict(zip(keys, pattern)) return pattern, LookupTable(table) @classmethod def mutate_value(cls, value): return value.flip() def mutate_table(self, table, mutation_probability): randoms = self._random.random(len(table.keys())) # Flip each value with a probability proportional to the mutation rate for i, (history, move) in enumerate(table.items()): if randoms[i] < mutation_probability: table[history] = self.mutate_value(move) return table
[docs] def mutate(self): lookup_dict = self.mutate_table( self.lookup_dict, self.mutation_probability ) # Add in starting moves initial_actions = list(self.initial_actions) for i in range(len(initial_actions)): r = self._random.random() if r < self.mutation_probability: initial_actions[i] = initial_actions[i].flip() return self.create_new( lookup_dict=lookup_dict, initial_actions=tuple(initial_actions), )
[docs] def crossover(self, other): if other.__class__ != self.__class__: raise TypeError( "Crossover must be between the same player classes." ) lookup_dict = crossover_dictionaries( self.lookup_dict, other.lookup_dict, self._random ) return self.create_new(lookup_dict=lookup_dict)
[docs]class EvolvedLookerUp1_1_1(LookerUp): """ A 1 1 1 Lookerup trained with an evolutionary algorithm. Names: - Evolved Lookerup 1 1 1: Original name by Marc Harper """ name = "EvolvedLookerUp1_1_1" def __init__(self) -> None: params = Plays(self_plays=1, op_plays=1, op_openings=1) super().__init__( parameters=params, pattern="CDDDDCDD", initial_actions=(C,) )
[docs]class EvolvedLookerUp2_2_2(LookerUp): """ A 2 2 2 Lookerup trained with an evolutionary algorithm. Names: - Evolved Lookerup 2 2 2: Original name by Marc Harper """ name = "EvolvedLookerUp2_2_2" def __init__(self) -> None: params = Plays(self_plays=2, op_plays=2, op_openings=2) pattern = ( "CDDCDCDDCDDDCDDDDDCDCDCCCDDCCDCDDDCCCCCDDDCDDDDDDDDDCCDDCDDDCCCD" ) super().__init__( parameters=params, pattern=pattern, initial_actions=(C, C) )
[docs]class Winner12(LookerUp): """ A lookup table based strategy. Names: - Winner12: [Mathieu2015]_ """ name = "Winner12" def __init__(self) -> None: params = Plays(self_plays=1, op_plays=2, op_openings=0) pattern = "CDCDDCDD" super().__init__( parameters=params, pattern=pattern, initial_actions=(C, C) )
[docs]class Winner21(LookerUp): """ A lookup table based strategy. Names: - Winner21: [Mathieu2015]_ """ name = "Winner21" def __init__(self) -> None: params = Plays(self_plays=1, op_plays=2, op_openings=0) pattern = "CDCDCDDD" super().__init__( parameters=params, pattern=pattern, initial_actions=(D, C) )
[docs]def get_last_n_plays(player: Player, depth: int) -> tuple: """Returns the last N plays of player as a tuple.""" if depth == 0: return () return tuple(player.history[-1 * depth :])