text:
"""
Additional strategies from Axelrod's second tournament.
"""

import random
from typing import List

import numpy as np
from axelrod.action import Action
from axelrod.interaction_utils import compute_final_score
from axelrod.player import Player
from axelrod.random_ import random_choice
from axelrod.strategies.finite_state_machines import FSMPlayer

C, D = Action.C, Action.D

class Champion(Player):
    """
    Strategy submitted to Axelrod's second tournament by Danny Champion.

    This player cooperates on the first 10 moves and plays Tit for Tat for the
    next 15 more moves. After 25 moves, the program cooperates unless all the
    following are true: the other player defected on the previous move, the
    other player cooperated less than 60% and the random number between 0 and 1
    is greater that the other player's cooperation rate.

    Names:

    - Champion: [Axelrod1980b]_
    """

    name = "Champion"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": True,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def strategy(self, opponent: Player) -> Action:
        current_round = len(self.history)
        # Cooperate for the first 10 turns
        if current_round == 0:
            return C
        if current_round < 10:
            return C
        # Mirror partner for the next phase
        if current_round < 25:
            return opponent.history[-1]
        # Now cooperate unless all of the necessary conditions are true
        defection_prop = opponent.defections / len(opponent.history)
        if opponent.history[-1] == D:
            r = random.random()
            if defection_prop >= max(0.4, r):
                return D
        return C

class Eatherley(Player):
    """
    Strategy submitted to Axelrod's second tournament by Graham Eatherley.

    A player that keeps track of how many times in the game the other player
    defected. After the other player defects, it defects with a probability
    equal to the ratio of the other's total defections to the total moves to
    that point.

    Names:

    - Eatherley: [Axelrod1980b]_
    """

    name = "Eatherley"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": True,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    @staticmethod
    def strategy(opponent: Player) -> Action:
        # Cooperate on the first move
        if not len(opponent.history):
            return C
        # Reciprocate cooperation
        if opponent.history[-1] == C:
            return C
        # Respond to defections with probability equal to opponent's total
        # proportion of defections
        defection_prop = opponent.defections / len(opponent.history)
        return random_choice(1 - defection_prop)

class Tester(Player):
    """
    Submitted to Axelrod's second tournament by David Gladstein.

    This strategy is a TFT variant that attempts to exploit certain strategies. It
    defects on the first move. If the opponent ever defects, TESTER 'apologies' by
    cooperating and then plays TFT for the rest of the game. Otherwise TESTER
    alternates cooperation and defection.

    This strategy came 46th in Axelrod's second tournament.

    Names:

    - Tester: [Axelrod1980b]_
    """

    name = "Tester"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": False,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self) -> None:
        super().__init__()
        self.is_TFT = False

    def strategy(self, opponent: Player) -> Action:
        # Defect on the first move
        if not opponent.history:
            return D
        # Am I TFT?
        if self.is_TFT:
            return D if opponent.history[-1:] == [D] else C
        else:
            # Did opponent defect?
            if opponent.history[-1] == D:
                self.is_TFT = True
                return C
            if len(self.history) in [1, 2]:
                return C
            # Alternate C and D
            return self.history[-1].flip()

class Gladstein(Player):
    """
    Submitted to Axelrod's second tournament by David Gladstein.

    This strategy is also known as Tester and is based on the reverse
    engineering of the Fortran strategies from Axelrod's second tournament.

    This strategy is a TFT variant that defects on the first round in order to
    test the opponent's response. If the opponent ever defects, the strategy
    'apologizes' by cooperating and then plays TFT for the rest of the game.
    Otherwise, it defects as much as possible subject to the constraint that
    the ratio of its defections to moves remains under 0.5, not counting the
    first defection.

    Names:

    - Gladstein: [Axelrod1980b]_
    - Tester: [Axelrod1980b]_
    """

    name = "Gladstein"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": False,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self) -> None:
        super().__init__()
        # This strategy assumes the opponent is a patsy
        self.patsy = True

    def strategy(self, opponent: Player) -> Action:
        # Defect on the first move
        if not self.history:
            return D
        # Is the opponent a patsy?
        if self.patsy:
            # If the opponent defects, apologize and play TFT.
            if opponent.history[-1] == D:
                self.patsy = False
                return C
            # Cooperate as long as the cooperation ratio is below 0.5
            cooperation_ratio = self.cooperations / len(self.history)
            if cooperation_ratio > 0.5:
                return D
            return C
        else:
            # Play TFT
            return opponent.history[-1]

class Tranquilizer(Player):

    """
    Submitted to Axelrod's second tournament by Craig Feathers

    Description given in Axelrod's "More Effective Choice in the
    Prisoner's Dilemma" paper: The rule normally cooperates but
    is ready to defect if the other player defects too often.
    Thus the rule tends to cooperate for the first dozen or two moves
    if the other player is cooperating, but then it throws in a
    defection. If the other player continues to cooperate, then defections
    become more frequent. But as long as Tranquilizer is maintaining an
    average payoff of at least 2.25 points per move, it will never defect
    twice in succession and it will not defect more than
    one-quarter of the time.

    This implementation is based on the reverse engineering of the
    Fortran strategy K67R from Axelrod's second tournament.
    Reversed engineered by: Owen Campbell, Will Guo and Mansour Hakem.

    The strategy starts by cooperating and has 3 states.

    At the start of the strategy it updates its states:

    - It counts the number of consecutive defections by the opponent.
    - If it was in state 2 it moves to state 0 and calculates the
      following quantities two_turns_after_good_defection_ratio and
      two_turns_after_good_defection_ratio_count.

      Formula for:

      two_turns_after_good_defection_ratio:

      self.two_turns_after_good_defection_ratio = (
      ((self.two_turns_after_good_defection_ratio
      * self.two_turns_after_good_defection_ratio_count)
      + (3 - (3 * self.dict[opponent.history[-1]]))
      + (2 * self.dict[self.history[-1]])
      - ((self.dict[opponent.history[-1]]
      * self.dict[self.history[-1]])))
      / (self.two_turns_after_good_defection_ratio_count + 1)
      )

      two_turns_after_good_defection_ratio_count =
      two_turns_after_good_defection_ratio + 1

    - If it was in state 1 it moves to state 2 and calculates the
      following quantities one_turn_after_good_defection_ratio and
      one_turn_after_good_defection_ratio_count.

      Formula for:

      one_turn_after_good_defection_ratio:

      self.one_turn_after_good_defection_ratio = (
      ((self.one_turn_after_good_defection_ratio
      * self.one_turn_after_good_defection_ratio_count)
      + (3 - (3 * self.dict[opponent.history[-1]]))
      + (2 * self.dict[self.history[-1]])
      - (self.dict[opponent.history[-1]]
      * self.dict[self.history[-1]]))
      / (self.one_turn_after_good_defection_ratio_count + 1)
      )

      one_turn_after_good_defection_ratio_count:

      one_turn_after_good_defection_ratio_count =
      one_turn_after_good_defection_ratio + 1

    If after this it is in state 1 or 2 then it cooperates.

    If it is in state 0 it will potentially perform 1 of the 2
    following stochastic tests:

    1. If average score per turn is greater than 2.25 then it calculates a
    value of probability:

    probability = (
    (.95 - (((self.one_turn_after_good_defection_ratio)
    + (self.two_turns_after_good_defection_ratio) - 5) / 15))
    + (1 / (((len(self.history))+1) ** 2))
    - (self.dict[opponent.history[-1]] / 4)
    )

    and will cooperate if a random sampled number is less than that value of
    probability. If it does not cooperate then the strategy moves to state 1
    and defects.

    2. If average score per turn is greater than 1.75 but less than 2.25
    then it calculates a value of probability:

    probability = (
    (.25 + ((opponent.cooperations + 1) / ((len(self.history)) + 1)))
    - (self.opponent_consecutive_defections * .25)
    + ((current_score[0]
    - current_score[1]) / 100)
    + (4 / ((len(self.history)) + 1))
    )

    and will cooperate if a random sampled number is less than that value of
    probability. If not, it defects.

    If none of the above holds the player simply plays tit for tat.

    Tranquilizer came in 27th place in Axelrod's second torunament.

    Names:

    - Tranquilizer: [Axelrod1980]_
    """

    name = "Tranquilizer"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": True,
        "makes_use_of": {"game"},
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self):
        super().__init__()
        self.num_turns_after_good_defection = 0  # equal to FD variable
        self.opponent_consecutive_defections = 0  # equal to S variable
        self.one_turn_after_good_defection_ratio = 5  # equal to AD variable
        self.two_turns_after_good_defection_ratio = 0  # equal to NO variable
        self.one_turn_after_good_defection_ratio_count = 1  # equal to AK variable
        self.two_turns_after_good_defection_ratio_count = 1  # equal to NK variable
        # All above variables correspond to those in original Fotran Code
        self.dict = {C: 0, D: 1}

    def update_state(self, opponent):

        """
        Calculates the ratio values for the one_turn_after_good_defection_ratio,
        two_turns_after_good_defection_ratio and the probability values,
        and sets the value of num_turns_after_good_defection.
        """
        if opponent.history[-1] == D:
            self.opponent_consecutive_defections += 1
        else:
            self.opponent_consecutive_defections = 0

        if self.num_turns_after_good_defection == 2:
            self.num_turns_after_good_defection = 0
            self.two_turns_after_good_defection_ratio = (
                (
                    self.two_turns_after_good_defection_ratio
                    * self.two_turns_after_good_defection_ratio_count
                )
                + (3 - (3 * self.dict[opponent.history[-1]]))
                + (2 * self.dict[self.history[-1]])
                - ((self.dict[opponent.history[-1]] * self.dict[self.history[-1]]))
            ) / (self.two_turns_after_good_defection_ratio_count + 1)
            self.two_turns_after_good_defection_ratio_count += 1
        elif self.num_turns_after_good_defection == 1:
            self.num_turns_after_good_defection = 2
            self.one_turn_after_good_defection_ratio = (
                (
                    self.one_turn_after_good_defection_ratio
                    * self.one_turn_after_good_defection_ratio_count
                )
                + (3 - (3 * self.dict[opponent.history[-1]]))
                + (2 * self.dict[self.history[-1]])
                - (self.dict[opponent.history[-1]] * self.dict[self.history[-1]])
            ) / (self.one_turn_after_good_defection_ratio_count + 1)
            self.one_turn_after_good_defection_ratio_count += 1

    def strategy(self, opponent: Player) -> Action:

        if not self.history:
            return C

        self.update_state(opponent)
        if self.num_turns_after_good_defection in [1, 2]:
            return C

        current_score = compute_final_score(zip(self.history, opponent.history))

        if (current_score[0] / ((len(self.history)) + 1)) >= 2.25:
            probability = (
                (
                    0.95
                    - (
                        (
                            (self.one_turn_after_good_defection_ratio)
                            + (self.two_turns_after_good_defection_ratio)
                            - 5
                        )
                        / 15
                    )
                )
                + (1 / (((len(self.history)) + 1) ** 2))
                - (self.dict[opponent.history[-1]] / 4)
            )
            if random.random() <= probability:
                return C
            self.num_turns_after_good_defection = 1
            return D
        if (current_score[0] / ((len(self.history)) + 1)) >= 1.75:
            probability = (
                (0.25 + ((opponent.cooperations + 1) / ((len(self.history)) + 1)))
                - (self.opponent_consecutive_defections * 0.25)
                + ((current_score[0] - current_score[1]) / 100)
                + (4 / ((len(self.history)) + 1))
            )
            if random.random() <= probability:
                return C
            return D
        return opponent.history[-1]

class MoreGrofman(Player):
    """
    Submitted to Axelrod's second tournament by Bernard Grofman.

    This strategy has 3 phases:

    1. First it cooperates on the first two rounds
    2. For rounds 3-7 inclusive, it plays the same as the opponent's last move
    3. Thereafter, it applies the following logic, looking at its memory of the
       last 8\* rounds (ignoring the most recent round).

      - If its own previous move was C and the opponent has defected less than
        3 times in the last 8\* rounds, cooperate
      - If its own previous move was C and the opponent has defected 3 or
        more times in the last 8\* rounds, defect
      - If its own previous move was D and the opponent has defected only once
        or not at all in the last 8\* rounds, cooperate
      - If its own previous move was D and the opponent has defected more than
        once in the last 8\* rounds, defect

    \* The code looks at the first 7 of the last 8 rounds, ignoring the most
    recent round.

    Names:
    - Grofman's strategy: [Axelrod1980b]_
    - K86R: [Axelrod1980b]_
    """

    name = "MoreGrofman"
    classifier = {
        "memory_depth": 8,
        "stochastic": False,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def strategy(self, opponent: Player) -> Action:
        # Cooperate on the first two moves
        if len(self.history) < 2:
            return C
        # For rounds 3-7, play the opponent's last move
        elif 2 <= len(self.history) <= 6:
            return opponent.history[-1]
        else:
            # Note: the Fortran code behavior ignores the opponent behavior
            #   in the last round and instead looks at the first 7 of the last
            #   8 rounds.
            opponent_defections_last_8_rounds = opponent.history[-8:-1].count(D)
            if self.history[-1] == C and opponent_defections_last_8_rounds <= 2:
                return C
            if self.history[-1] == D and opponent_defections_last_8_rounds <= 1:
                return C
            return D

class Kluepfel(Player):
    """
    Strategy submitted to Axelrod's second tournament by Charles Kluepfel
    (K32R).

    This player keeps track of the the opponent's responses to own behavior:

    - `cd_count` counts: Opponent cooperates as response to player defecting.
    - `dd_count` counts: Opponent defects as response to player defecting.
    - `cc_count` counts: Opponent cooperates as response to player cooperating.
    - `dc_count` counts: Opponent defects as response to player cooperating.

    After 26 turns, the player then tries to detect a random player.  The
    player decides that the opponent is random if
    cd_counts >= (cd_counts+dd_counts)/2 - 0.75*sqrt(cd_counts+dd_counts) AND
    cc_counts >= (dc_counts+cc_counts)/2 - 0.75*sqrt(dc_counts+cc_counts).
    If the player decides that they are playing against a random player, then
    they will always defect.

    Otherwise respond to recent history using the following set of rules:

    - If opponent's last three choices are the same, then respond in kind.
    - If opponent's last two choices are the same, then respond in kind with
      probability 90%.
    - Otherwise if opponent's last action was to cooperate, then cooperate
      with probability 70%.
    - Otherwise if opponent's last action was to defect, then defect
      with probability 60%.

    Names:

    - Kluepfel: [Axelrod1980b]_
    """

    name = "Kluepfel"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": True,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self):
        super().__init__()
        self.cd_counts, self.dd_counts, self.dc_counts, self.cc_counts = 0, 0, 0, 0

    def strategy(self, opponent: Player) -> Action:
        # First update the response matrix.
        if len(self.history) >= 2:
            if self.history[-2] == D:
                if opponent.history[-1] == C:
                    self.cd_counts += 1
                else:
                    self.dd_counts += 1
            else:
                if opponent.history[-1] == C:
                    self.dc_counts += 1
                else:
                    self.cc_counts += 1

        # Check for randomness
        if len(self.history) > 26:
            if self.cd_counts >= (self.cd_counts + self.dd_counts) / 2 - 0.75 * np.sqrt(
                self.cd_counts + self.dd_counts
            ) and self.cc_counts >= (
                self.dc_counts + self.cc_counts
            ) / 2 - 0.75 * np.sqrt(
                self.dc_counts + self.cc_counts
            ):
                return D

        # Otherwise respond to recent history

        one_move_ago, two_moves_ago, three_moves_ago = C, C, C
        if len(opponent.history) >= 1:
            one_move_ago = opponent.history[-1]
        if len(opponent.history) >= 2:
            two_moves_ago = opponent.history[-2]
        if len(opponent.history) >= 3:
            three_moves_ago = opponent.history[-3]

        if one_move_ago == two_moves_ago and two_moves_ago == three_moves_ago:
            return one_move_ago

        r = random.random()  # Everything following is stochastic
        if one_move_ago == two_moves_ago:
            if r < 0.9:
                return one_move_ago
            else:
                return one_move_ago.flip()
        if one_move_ago == C:
            if r < 0.7:
                return one_move_ago
            else:
                return one_move_ago.flip()
        if one_move_ago == D:
            if r < 0.6:
                return one_move_ago
            else:
                return one_move_ago.flip()

class Borufsen(Player):
    """
    Strategy submitted to Axelrod's second tournament by Otto Borufsen
    (K32R), and came in third in that tournament.

    This player keeps track of the the opponent's responses to own behavior:

    - `cd_count` counts: Opponent cooperates as response to player defecting.
    - `cc_count` counts: Opponent cooperates as response to player cooperating.

    The player has a defect mode and a normal mode.  In defect mode, the
    player will always defect.  In normal mode, the player obeys the following
    ranked rules:

    1. If in the last three turns, both the player/opponent defected, then
       cooperate for a single turn.
    2. If in the last three turns, the player/opponent acted differently from
       each other and they're alternating, then change next defect to
       cooperate.  (Doesn't block third rule.)
    3. Otherwise, do tit-for-tat.

    Start in normal mode, but every 25 turns starting with the 27th turn,
    re-evaluate the mode.  Enter defect mode if any of the following
    conditions hold:

    - Detected random:  Opponent cooperated 7-18 times since last mode
      evaluation (or start) AND less than 70% of opponent cooperation was in
      response to player's cooperation, i.e.
      cc_count / (cc_count+cd_count) < 0.7
    - Detect defective:  Opponent cooperated fewer than 3 times since last mode
      evaluation.

    When switching to defect mode, defect immediately.  The first two rules for
    normal mode require that last three turns were in normal mode.  When starting
    normal mode from defect mode, defect on first move.

    Names:

    - Borufsen: [Axelrod1980b]_
    """

    name = "Borufsen"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": False,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self):
        super().__init__()
        self.cd_counts, self.cc_counts = 0, 0
        self.mutual_defect_streak = 0
        self.echo_streak = 0
        self.flip_next_defect = False
        self.mode = "Normal"

    def try_return(self, to_return):
        """
        We put the logic here to check for the `flip_next_defect` bit here,
        and proceed like normal otherwise.
        """

        if to_return == C:
            return C
        # Otherwise look for flip bit.
        if self.flip_next_defect:
            self.flip_next_defect = False
            return C
        return D

    def strategy(self, opponent: Player) -> Action:
        turn = len(self.history) + 1

        if turn == 1:
            return C

        # Update the response history.
        if turn >= 3:
            if opponent.history[-1] == C:
                if self.history[-2] == C:
                    self.cc_counts += 1
                else:
                    self.cd_counts += 1

        # Check if it's time for a mode change.
        if turn > 2 and turn % 25 == 2:
            coming_from_defect = False
            if self.mode == "Defect":
                coming_from_defect = True

            self.mode = "Normal"
            coops = self.cd_counts + self.cc_counts

            # Check for a defective strategy
            if coops < 3:
                self.mode = "Defect"

            # Check for a random strategy
            if (coops >= 8 and coops <= 17) and self.cc_counts / coops < 0.7:
                self.mode = "Defect"

            self.cd_counts, self.cc_counts = 0, 0

            # If defect mode, clear flags
            if self.mode == "Defect":
                self.mutual_defect_streak = 0
                self.echo_streak = 0
                self.flip_next_defect = False

            # Check this special case
            if self.mode == "Normal" and coming_from_defect:
                return D

        # Proceed
        if self.mode == "Defect":
            return D
        else:
            assert self.mode == "Normal"

            # Look for mutual defects
            if self.history[-1] == D and opponent.history[-1] == D:
                self.mutual_defect_streak += 1
            else:
                self.mutual_defect_streak = 0
            if self.mutual_defect_streak >= 3:
                self.mutual_defect_streak = 0
                self.echo_streak = 0  # Reset both streaks.
                return self.try_return(C)

            # Look for echoes
            # Fortran code defaults two turns back to C if only second turn
            my_two_back, opp_two_back = C, C
            if turn >= 3:
                my_two_back = self.history[-2]
                opp_two_back = opponent.history[-2]
            if (
                self.history[-1] != opponent.history[-1]
                and self.history[-1] == opp_two_back
                and opponent.history[-1] == my_two_back
            ):
                self.echo_streak += 1
            else:
                self.echo_streak = 0
            if self.echo_streak >= 3:
                self.mutual_defect_streak = 0  # Reset both streaks.
                self.echo_streak = 0
                self.flip_next_defect = True

            # Tit-for-tat
            return self.try_return(opponent.history[-1])

class Cave(Player):
    """
    Strategy submitted to Axelrod's second tournament by Rob Cave (K49R), and
    came in fourth in that tournament.

    First look for overly-defective or apparently random opponents, and defect
    if found.  That is any opponent meeting one of:

    - turn > 39 and percent defects > 0.39
    - turn > 29 and percent defects > 0.65
    - turn > 19 and percent defects > 0.79

    Otherwise, respond to cooperation with cooperation.  And respond to defcts
    with either a defect (if opponent has defected at least 18 times) or with
    a random (50/50) choice.  [Cooperate on first.]

    Names:

    - Cave: [Axelrod1980b]_
    """

    name = "Cave"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": True,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def strategy(self, opponent: Player) -> Action:
        turn = len(self.history) + 1
        if turn == 1:
            return C

        number_defects = opponent.defections
        perc_defects = number_defects / turn

        # Defect if the opponent has defected often or appears random.
        if turn > 39 and perc_defects > 0.39:
            return D
        if turn > 29 and perc_defects > 0.65:
            return D
        if turn > 19 and perc_defects > 0.79:
            return D

        if opponent.history[-1] == D:
            if number_defects > 17:
                return D
            else:
                return random_choice(0.5)
        else:
            return C

class WmAdams(Player):
    """
    Strategy submitted to Axelrod's second tournament by William Adams (K44R),
    and came in fifth in that tournament.

    Count the number of opponent defections after their first move, call
    `c_defect`.  Defect if c_defect equals 4, 7, or 9.  If c_defect > 9,
    then defect immediately after opponent defects with probability =
    (0.5)^(c_defect-1).  Otherwise cooperate.

    Names:

    - WmAdams: [Axelrod1980b]_
    """

    name = "WmAdams"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": True,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def strategy(self, opponent: Player) -> Action:
        if len(self.history) <= 1:
            return C
        number_defects = opponent.defections
        if opponent.history[0] == D:
            number_defects -= 1

        if number_defects in [4, 7, 9]:
            return D
        if number_defects > 9 and opponent.history[-1] == D:
            return random_choice((0.5) ** (number_defects - 9))
        return C

class GraaskampKatzen(Player):
    """
    Strategy submitted to Axelrod's second tournament by Jim Graaskamp and Ken
    Katzen (K60R), and came in sixth in that tournament.

    Play Tit-for-Tat at first, and track own score.  At select checkpoints,
    check for a high score.  Switch to Default Mode if:

    - On move 11, score < 23
    - On move 21, score < 53
    - On move 31, score < 83
    - On move 41, score < 113
    - On move 51, score < 143
    - On move 101, score < 293

    Once in Defect Mode, defect forever.

    Names:

    - GraaskampKatzen: [Axelrod1980b]_
    """

    name = "GraaskampKatzen"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": False,
        "makes_use_of": set(["game"]),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self):
        super().__init__()
        self.own_score = 0
        self.mode = "Normal"

    def update_score(self, opponent: Player):
        game = self.match_attributes["game"]
        last_round = (self.history[-1], opponent.history[-1])
        self.own_score += game.score(last_round)[0]

    def strategy(self, opponent: Player) -> Action:
        if self.mode == "Defect":
            return D

        turn = len(self.history) + 1
        if turn == 1:
            return C

        self.update_score(opponent)

        if (
            turn == 11
            and self.own_score < 23
            or turn == 21
            and self.own_score < 53
            or turn == 31
            and self.own_score < 83
            or turn == 41
            and self.own_score < 113
            or turn == 51
            and self.own_score < 143
            or turn == 101
            and self.own_score < 293
        ):
            self.mode = "Defect"
            return D

        return opponent.history[-1]  # Tit-for-Tat

class Weiner(Player):
    """
    Strategy submitted to Axelrod's second tournament by Herb Weiner (K41R),
    and came in seventh in that tournament.

    Play Tit-for-Tat with a chance for forgiveness and a defective override.

    The chance for forgiveness happens only if `forgive_flag` is raised
    (flag discussed below).  If raised and `turn` is greater than `grudge`,
    then override Tit-for-Tat with Cooperation.  `grudge` is a variable that
    starts at 0 and increments 20 with each forgiven Defect (a Defect that is
    overriden through the forgiveness logic).  `forgive_flag` is lower whether
    logic is overriden or not.

    The variable `defect_padding` increments with each opponent Defect, but
    resets to zero with each opponent Cooperate (or `forgive_flag` lowering) so
    that it roughly counts Defects between Cooperates.  Whenever the opponent
    Cooperates, if `defect_padding` (before reseting) is odd, then we raise
    `forgive_flag` for next turn.

    Finally a defective override is assessed after forgiveness.  If five or
    more of the opponent's last twelve actions are Defects, then Defect.  This
    will overrule a forgiveness, but doesn't undo the lowering of
    `forgiveness_flag`.  Note that "last twelve actions" doesn't count the most
    recent action.  Actually the original code updates history after checking
    for defect override.

    Names:

    - Weiner: [Axelrod1980b]_
    """

    name = "Weiner"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": False,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self):
        super().__init__()
        self.forgive_flag = False
        self.grudge = 0
        self.defect_padding = 0
        self.last_twelve = [0] * 12
        self.lt_index = 0  # Circles around last_twelve

    def try_return(self, to_return):
        """
        We put the logic here to check for the defective override.
        """

        if np.sum(self.last_twelve) >= 5:
            return D
        return to_return

    def strategy(self, opponent: Player) -> Action:
        if len(opponent.history) == 0:
            return C

        # Update history, lag 1.
        if len(opponent.history) >= 2:
            self.last_twelve[self.lt_index] = 0
            if opponent.history[-2] == D:
                self.last_twelve[self.lt_index] = 1
            self.lt_index = (self.lt_index + 1) % 12

        if self.forgive_flag:
            self.forgive_flag = False
            self.defect_padding = 0
            if self.grudge < len(self.history) + 1 and opponent.history[-1] == D:
                # Then override
                self.grudge += 20
                return self.try_return(C)
            else:
                return self.try_return(opponent.history[-1])
        else:
            # See if forgive_flag should be raised
            if opponent.history[-1] == D:
                self.defect_padding += 1
            else:
                if self.defect_padding % 2 == 1:
                    self.forgive_flag = True
                self.defect_padding = 0

            return self.try_return(opponent.history[-1])

class Harrington(Player):
    """
    Strategy submitted to Axelrod's second tournament by Paul Harrington (K75R)
    and came in eighth in that tournament.

    This strategy has three modes:  Normal, Fair-weather, and Defect.  These
    mode names were not present in Harrington's submission.

    In Normal and Fair-weather modes, the strategy begins by:

    - Update history
    - Try to detect random opponent if turn is multiple of 15 and >=30.
    - Check if `burned` flag should be raised.
    - Check for Fair-weather opponent if turn is 38.

    Updating history means to increment the correct cell of the `move_history`.
    `move_history` is a matrix where the columns are the opponent's previous
    move and the rows are indexed by the combo of this player's and the
    opponent's moves two turns ago.  [The upper-left cell must be all
    Cooperations, but otherwise order doesn't matter.]  After we enter Defect
    mode, `move_history` won't be used again.

    If the turn is a multiple of 15 and >=30, then attempt to detect random.
    If random is detected, enter Defect mode and defect immediately.  If the
    player was previously in Defect mode, then do not re-enter.  The random
    detection logic is a modified Pearson's Chi Squared test, with some
    additional checks.  [More details in `detect_random` docstrings.]

    Some of this player's moves are marked as "generous."  If this player made
    a generous move two turns ago and the opponent replied with a Defect, then
    raise the `burned` flag.  This will stop certain generous moves later.

    The player mostly plays Tit-for-Tat for the first 36 moves, then defects on
    the 37th move.  If the opponent cooperates on the first 36 moves, and
    defects on the 37th move also, then enter Fair-weather mode and cooperate
    this turn.  Entering Fair-weather mode is extremely rare, since this can
    only happen if the opponent cooperates for the first 36 then defects
    unprovoked on the 37th.  (That is, this player's first 36 moves are also
    Cooperations, so there's nothing really to trigger an opponent Defection.)

    Next in Normal Mode:

    1. Check for defect and parity streaks.
    2. Check if cooperations are scheduled.
    3. Otherwise,

    - If turn < 37, Tit-for-Tat.
    - If turn = 37, defect, mark this move as generous, and schedule two
      more cooperations**.
    - If turn > 37, then if `burned` flag is raised, then Tit-for-Tat.
      Otherwise, Tit-for-Tat with probability 1 - `prob`.  And with
      probability `prob`, defect, schedule two cooperations, mark this move
      as generous, and increase `prob` by 5%.

    ** Scheduling two cooperations means to set `more_coop` flag to two.  If in
    Normal mode and no streaks are detected, then the player will cooperate and
    lower this flag, until hitting zero.  It's possible that the flag can be
    overwritten.  Notable on the 37th turn defect, this is set to two, but the
    38th turn Fair-weather check will set this.

    If the opponent's last twenty moves were defections, then defect this turn.
    Then check for a parity streak, by flipping the parity bit (there are two
    streaks that get tracked which are something like odd and even turns, but
    this flip bit logic doesn't get run every turn), then incrementing the
    parity streak that we're pointing to.  If the parity streak that we're
    pointing to is then greater than `parity_limit` then reset the streak and
    cooperate immediately.  `parity_limit` is initially set to five, but after
    it has been hit eight times, it decreases to three.  The parity streak that
    we're pointing to also gets incremented if in normal mode and we defect but
    not on turn 38, unless we are defecting as the result of a defect streak.
    Note that the parity streaks resets but the defect streak doesn't.

    If `more_coop` >= 1, then we cooperate and lower that flag here, in Normal
    mode after checking streaks.  Still lower this flag if cooperating as the
    result of a parity streak or in Fair-weather mode.

    Then use the logic based on turn from above.

    In Fair-Weather mode after running the code from above, check if opponent
    defected last turn.  If so, exit Fair-Weather mode, and proceed THIS TURN
    with Normal mode.  Otherwise cooperate.

    In Defect mode, update the `exit_defect_meter` (originally zero) by
    incrementing if opponent defected last turn and decreasing by three
    otherwise.  If `exit_defect_meter` is then 11, then set mode to Normal (for
    future turns), cooperate and schedule two more cooperations.  [Note that
    this move is not marked generous.]

    Names:

    - Harrington: [Axelrod1980b]_
    """

    name = "Harrington"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": True,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self):
        super().__init__()
        self.mode = "Normal"
        self.recorded_defects = 0  # Count opponent defects after turn 1
        self.exit_defect_meter = 0  # When >= 11, then exit defect mode.
        self.coops_in_first_36 = None  # On turn 37, count cooperations in first 36
        self.was_defective = False  # Previously in Defect mode

        self.prob = 0.25  # After turn 37, probability that we'll defect

        self.move_history = np.zeros([4, 2])

        self.more_coop = 0  # This schedules cooperation for future turns
        # Initial last_generous_n_turns_ago to 3 because this counts up and
        # triggers a strategy change at 2.
        self.last_generous_n_turns_ago = 3  # How many tuns ago was a "generous" move
        self.burned = False

        self.defect_streak = 0
        self.parity_streak = [
            0,
            0,
        ]  # Counters that get (almost) alternatively incremented.
        self.parity_bit = 0  # Which parity_streak to increment
        self.parity_limit = 5  # When a parity streak hits this limit, alter strategy.
        self.parity_hits = 0  # Counts how many times a parity_limit was hit.
        # After hitting parity_hits 8 times, lower parity_limit to 3.

    def try_return(self, to_return, lower_flags=True, inc_parity=False):
        """
        This will return to_return, with some end-of-turn logic.
        """

        if lower_flags and to_return == C:
            # In most cases when Cooperating, we want to reduce the number that
            # are scheduled.
            self.more_coop -= 1
            self.last_generous_n_turns_ago += 1

        if inc_parity and to_return == D:
            # In some cases we increment the `parity_streak` that we're on when
            # we return a Defection.  In detect_parity_streak, `parity_streak`
            # counts opponent's Defections.
            self.parity_streak[self.parity_bit] += 1

        return to_return

    def calculate_chi_squared(self, turn):
        """
        Pearson's Chi Squared statistic = sum[ (E_i-O_i)^2 / E_i ], where O_i
        are the observed matrix values, and E_i is calculated as number (of
        defects) in the row times the number in the column over (total number
        in the matrix minus 1).  Equivalently, we expect we expect (for an
        independent distribution) the total number of recorded turns times the
        portion in that row times the portion in that column.

        In this function, the statistic is non-standard in that it excludes
        summands where E_i <= 1.
        """

        denom = turn - 2

        expected_matrix = (
            np.outer(self.move_history.sum(axis=1), self.move_history.sum(axis=0))
            / denom
        )

        chi_squared = 0.0
        for i in range(4):
            for j in range(2):
                expect = expected_matrix[i, j]
                if expect > 1.0:
                    chi_squared += (expect - self.move_history[i, j]) ** 2 / expect

        return chi_squared

    def detect_random(self, turn):
        """
        We check if the top-left cell of the matrix (corresponding to all
        Cooperations) has over 80% of the turns.  In which case, we label
        non-random.

        Then we check if over 75% or under 25% of the opponent's turns are
        Defections.  If so, then we label as non-random.

        Otherwise we calculates a modified Pearson's Chi Squared statistic on
        self.history, and returns True (is random) if and only if the statistic
        is less than or equal to 3.
        """

        denom = turn - 2

        if self.move_history[0, 0] / denom >= 0.8:
            return False
        if self.recorded_defects / denom < 0.25 or self.recorded_defects / denom > 0.75:
            return False

        if self.calculate_chi_squared(turn) > 3:
            return False
        return True

    def detect_streak(self, last_move):
        """
        Return true if and only if the opponent's last twenty moves are defects.
        """

        if last_move == D:
            self.defect_streak += 1
        else:
            self.defect_streak = 0
        if self.defect_streak >= 20:
            return True
        return False

    def detect_parity_streak(self, last_move):
        """
        Switch which `parity_streak` we're pointing to and incerement if the
        opponent's last move was a Defection.  Otherwise reset the flag.  Then
        return true if and only if the `parity_streak` is at least
        `parity_limit`.

        This is similar to detect_streak with alternating streaks, except that
        these streaks get incremented elsewhere as well.
        """

        self.parity_bit = 1 - self.parity_bit  # Flip bit
        if last_move == D:
            self.parity_streak[self.parity_bit] += 1
        else:
            self.parity_streak[self.parity_bit] = 0
        if self.parity_streak[self.parity_bit] >= self.parity_limit:
            return True

    def strategy(self, opponent: Player) -> Action:
        turn = len(self.history) + 1

        if turn == 1:
            return C

        if self.mode == "Defect":
            # There's a chance to exit Defect mode.
            if opponent.history[-1] == D:
                self.exit_defect_meter += 1
            else:
                self.exit_defect_meter -= 3
            # If opponent has been mostly defecting.
            if self.exit_defect_meter >= 11:
                self.mode = "Normal"
                self.was_defective = True
                self.more_coop = 2
                return self.try_return(to_return=C, lower_flags=False)

            return self.try_return(D)

        # If not Defect mode, proceed to update history and check for random,
        # check if burned, and check if opponent's fairweather.

        # If we haven't yet entered Defect mode
        if not self.was_defective:
            if turn > 2:
                if opponent.history[-1] == D:
                    self.recorded_defects += 1

                # Column decided by opponent's last turn
                history_col = 1 if opponent.history[-1] == D else 0
                # Row is decided by opponent's move two turns ago and our move
                # two turns ago.
                history_row = 1 if opponent.history[-2] == D else 0
                if self.history[-2] == D:
                    history_row += 2
                self.move_history[history_row, history_col] += 1

            # Try to detect random opponent
            if turn % 15 == 0 and turn > 15:
                if self.detect_random(turn):
                    self.mode = "Defect"
                    return self.try_return(
                        D, lower_flags=False
                    )  # Lower_flags not used here.

        # If generous 2 turns ago and opponent defected last turn
        if self.last_generous_n_turns_ago == 2 and opponent.history[-1] == D:
            self.burned = True

        # Only enter Fair-weather mode if the opponent Cooperated the first 37
        # turns then Defected on the 38th.
        if turn == 38 and opponent.history[-1] == D and opponent.cooperations == 36:
            self.mode = "Fair-weather"
            return self.try_return(to_return=C, lower_flags=False)

        if self.mode == "Fair-weather":
            if opponent.history[-1] == D:
                self.mode = "Normal"  # Post-Defect is not possible
                # Proceed with Normal mode this turn.
            else:
                # Never defect against a fair-weather opponent
                return self.try_return(C)

        # Continue with Normal mode

        # Check for streaks
        if self.detect_streak(opponent.history[-1]):
            return self.try_return(D, inc_parity=True)
        if self.detect_parity_streak(opponent.history[-1]):
            self.parity_streak[
                self.parity_bit
            ] = 0  # Reset `parity_streak` when we hit the limit.
            self.parity_hits += 1  # Keep track of how many times we hit the limit.
            if self.parity_hits >= 8:  # After 8 times, lower the limit.
                self.parity_limit = 3
            return self.try_return(
                C, inc_parity=True
            )  # Inc parity won't get used here.

        # If we have Cooperations scheduled, then Cooperate here.
        if self.more_coop >= 1:
            return self.try_return(C, lower_flags=True, inc_parity=True)

        if turn < 37:
            # Tit-for-Tat
            return self.try_return(opponent.history[-1], inc_parity=True)
        if turn == 37:
            # Defect once on turn 37 (if no streaks)
            self.more_coop, self.last_generous_n_turns_ago = 2, 1
            return self.try_return(D, lower_flags=False)
        if self.burned or random.random() > self.prob:
            # Tit-for-Tat with probability 1-`prob`
            return self.try_return(opponent.history[-1], inc_parity=True)

        # Otherwise Defect, Cooperate, Cooperate, and increase `prob`
        self.prob += 0.05
        self.more_coop, self.last_generous_n_turns_ago = 2, 1
        return self.try_return(D, lower_flags=False)

class MoreTidemanAndChieruzzi(Player):
    """
    Strategy submitted to Axelrod's second tournament by T. Nicolaus Tideman
    and Paula Chieruzzi (K84R) and came in ninth in that tournament.

    This strategy Cooperates if this player's score exceeds the opponent's
    score by at least `score_to_beat`.  `score_to_beat` starts at zero and
    increases by `score_to_beat_inc` every time the opponent's last two moves
    are a Cooperation and Defection in that order.  `score_to_beat_inc` itself
    increase by 5 every time the opponent's last two moves are a Cooperation
    and Defection in that order.

    Additionally, the strategy executes a "fresh start" if the following hold:

    - The strategy would Defect by score (difference less than `score_to_beat`)
    - The opponent did not Cooperate and Defect (in order) in the last two
      turns.
    - It's been at least 10 turns since the last fresh start.  Or since the
      match started if there hasn't been a fresh start yet.

    A "fresh start" entails two Cooperations and resetting scores,
    `scores_to_beat` and `scores_to_beat_inc`.

    Names:

    - MoreTidemanAndChieruzzi: [Axelrod1980b]_
    """

    name = "More Tideman and Chieruzzi"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": False,
        "makes_use_of": {"game"},
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self) -> None:
        super().__init__()
        self.current_score = 0
        self.opponent_score = 0
        self.last_fresh_start = 0
        self.fresh_start = False
        self.score_to_beat = 0
        self.score_to_beat_inc = 0

    def _fresh_start(self):
        """Give the opponent a fresh start by forgetting the past"""
        self.current_score = 0
        self.opponent_score = 0
        self.score_to_beat = 0
        self.score_to_beat_inc = 0

    def _score_last_round(self, opponent: Player):
        """Updates the scores for each player."""
        # Load the default game if not supplied by a tournament.
        game = self.match_attributes["game"]
        last_round = (self.history[-1], opponent.history[-1])
        scores = game.score(last_round)
        self.current_score += scores[0]
        self.opponent_score += scores[1]

    def strategy(self, opponent: Player) -> Action:
        current_round = len(self.history) + 1

        if current_round == 1:
            return C

        # Calculate the scores.
        self._score_last_round(opponent)

        # Check if we have recently given the strategy a fresh start.
        if self.fresh_start:
            self._fresh_start()
            self.last_fresh_start = current_round
            self.fresh_start = False
            return C  # Second cooperation

        opponent_CDd = False

        opponent_two_turns_ago = C  # Default value for second turn.
        if len(opponent.history) >= 2:
            opponent_two_turns_ago = opponent.history[-2]
        # If opponent's last two turns are C and D in that order.
        if opponent_two_turns_ago == C and opponent.history[-1] == D:
            opponent_CDd = True
            self.score_to_beat += self.score_to_beat_inc
            self.score_to_beat_inc += 5

        # Cooperate if we're beating opponent by at least `score_to_beat`
        if self.current_score - self.opponent_score >= self.score_to_beat:
            return C

        # Wait at least ten turns for another fresh start.
        if (not opponent_CDd) and current_round - self.last_fresh_start >= 10:
            # 50-50 split is based off the binomial distribution.
            N = opponent.cooperations + opponent.defections
            # std_dev = sqrt(N*p*(1-p)) where p is 1 / 2.
            std_deviation = (N ** (1 / 2)) / 2
            lower = N / 2 - 3 * std_deviation
            upper = N / 2 + 3 * std_deviation
            if opponent.defections <= lower or opponent.defections >= upper:
                # Opponent deserves a fresh start
                self.fresh_start = True
                return C  # First cooperation

        return D

class Getzler(Player):
    """
    Strategy submitted to Axelrod's second tournament by Abraham Getzler (K35R)
    and came in eleventh in that tournament.

    Strategy Defects with probability `flack`, where `flack` is calculated as
    the sum over opponent Defections of 0.5 ^ (turns ago Defection happened).

    Names:

    - Getzler: [Axelrod1980b]_
    """

    name = "Getzler"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": True,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self) -> None:
        super().__init__()
        self.flack = 0.0  # The relative untrustworthiness of opponent

    def strategy(self, opponent: Player) -> Action:
        if not opponent.history:
            return C

        self.flack += 1 if opponent.history[-1] == D else 0
        self.flack *= 0.5  # Defections have half-life of one round

        return random_choice(1.0 - self.flack)

class Leyvraz(Player):
    """
    Strategy submitted to Axelrod's second tournament by Fransois Leyvraz
    (K68R) and came in twelfth in that tournament.

    The strategy uses the opponent's last three moves to decide on an action
    based on the following ordered rules.

    1. If opponent Defected last two turns, then Defect with prob 75%.
    2. If opponent Defected three turns ago, then Cooperate.
    3. If opponent Defected two turns ago, then Defect.
    4. If opponent Defected last turn, then Defect with prob 50%.
    5. Otherwise (all Cooperations), then Cooperate.

    Names:

    - Leyvraz: [Axelrod1980b]_
    """

    name = "Leyvraz"
    classifier = {
        "memory_depth": 3,
        "stochastic": True,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self) -> None:
        super().__init__()
        self.prob_coop = {
            (C, C, C): 1.0,
            (C, C, D): 0.5,  # Rule 4
            (C, D, C): 0.0,  # Rule 3
            (C, D, D): 0.25,  # Rule 1
            (D, C, C): 1.0,  # Rule 2
            (D, C, D): 1.0,  # Rule 2
            (D, D, C): 1.0,  # Rule 2
            (D, D, D): 0.25,  # Rule 1
        }

    def strategy(self, opponent: Player) -> Action:
        recent_history = [C, C, C]  # Default to C.
        for go_back in range(1, 4):
            if len(opponent.history) >= go_back:
                recent_history[-go_back] = opponent.history[-go_back]

        return random_choice(
            self.prob_coop[(recent_history[-3], recent_history[-2], recent_history[-1])]
        )

class White(Player):
    """
    Strategy submitted to Axelrod's second tournament by Edward C White (K72R)
    and came in thirteenth in that tournament.

    * If the opponent Cooperated last turn or in the first ten turns, then
      Cooperate.
    * Otherwise Defect if and only if:
        floor(log(turn)) * opponent Defections >= turn

    Names:

    - White: [Axelrod1980b]_
    """

    name = "White"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": False,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def strategy(self, opponent: Player) -> Action:
        turn = len(self.history) + 1

        if turn <= 10 or opponent.history[-1] == C:
            return C

        if np.floor(np.log(turn)) * opponent.defections >= turn:
            return D
        return C

class Black(Player):
    """
    Strategy submitted to Axelrod's second tournament by Paul E Black (K83R)
    and came in fifteenth in that tournament.

    The strategy Cooperates for the first five turns.  Then it calculates the
    number of opponent defects in the last five moves and Cooperates with
    probability `prob_coop`[`number_defects`], where:

    prob_coop[number_defects] = 1 - (number_defects^ 2 - 1) / 25

    Names:

    - Black: [Axelrod1980b]_
    """

    name = "Black"
    classifier = {
        "memory_depth": 5,
        "stochastic": True,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self) -> None:
        super().__init__()
        # Maps number of opponent defects from last five moves to own
        # Cooperation probability
        self.prob_coop = {0: 1.0, 1: 1.0, 2: 0.88, 3: 0.68, 4: 0.4, 5: 0.04}

    def strategy(self, opponent: Player) -> Action:
        if len(opponent.history) < 5:
            return C

        recent_history = opponent.history[-5:]

        did_d = np.vectorize(lambda action: int(action == D))
        number_defects = np.sum(did_d(recent_history))

        return random_choice(self.prob_coop[number_defects])

class RichardHufford(Player):
    """
    Strategy submitted to Axelrod's second tournament by Richard Hufford (K47R)
    and came in sixteenth in that tournament.

    The strategy tracks opponent "agreements", that is whenever the opponent's
    previous move is the some as this player's move two turns ago.  If the
    opponent's first move is a Defection, this is counted as a disagreement,
    and otherwise an agreement.  From the agreement counts, two measures are
    calculated:

    - `proportion_agree`:  This is the number of agreements (through opponent's
      last turn) + 2 divided by the current turn number.
    - `last_four_num`:  The number of agreements in the last four turns.  If
      there have been fewer than four previous turns, then this is number of
      agreement + (4 - number of past turns).

    We then use these measures to decide how to play, using these rules:

    1. If `proportion_agree` > 0.9 and `last_four_num` >= 4, then Cooperate.
    2. Otherwise if `proportion_agree` >= 0.625 and `last_four_num` >= 2, then
       Tit-for-Tat.
    3. Otherwise, Defect.

    However, if the opponent has Cooperated the last `streak_needed` turns,
    then the strategy deviates from the usual strategy, and instead Defects.
    (We call such deviation an "aberration".)  In the turn immediately after an
    aberration, the strategy doesn't override, even if there's a streak of
    Cooperations.  Two turns after an aberration, the strategy:  Restarts the
    Cooperation streak (never looking before this turn); Cooperates; and
    changes `streak_needed` to:

    floor(20.0 * `num_abb_def` / `num_abb_coop`) + 1

    Here `num_abb_def` is 2 + the number of times that the opponent Defected in
    the turn after an aberration, and `num_abb_coop` is 2 + the number of times
    that the opponent Cooperated in response to an aberration.

    Names:

    - RichardHufford: [Axelrod1980b]_
    """

    name = "RichardHufford"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": False,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self) -> None:
        super().__init__()
        self.num_agreements = 2
        self.last_four_agreements = [1] * 4
        self.last_four_index = 0

        self.streak_needed = 21
        self.current_streak = 2
        self.last_aberration = float("inf")
        self.coop_after_ab_count = 2
        self.def_after_ab_count = 2

    def strategy(self, opponent: Player) -> Action:
        turn = len(self.history) + 1
        if turn == 1:
            return C

        # Check if opponent agreed with us.
        self.last_four_index = (self.last_four_index + 1) % 4
        me_two_moves_ago = C
        if turn > 2:
            me_two_moves_ago = self.history[-2]
        if me_two_moves_ago == opponent.history[-1]:
            self.num_agreements += 1
            self.last_four_agreements[self.last_four_index] = 1
        else:
            self.last_four_agreements[self.last_four_index] = 0

        # Check if last_aberration is infinite.
        # i.e Not an aberration in last two turns.
        if turn < self.last_aberration:
            if opponent.history[-1] == C:
                self.current_streak += 1
            else:
                self.current_streak = 0
            if self.current_streak >= self.streak_needed:
                self.last_aberration = turn
                if self.current_streak == self.streak_needed:
                    return D
        elif turn == self.last_aberration + 2:
            self.last_aberration = float("inf")
            if opponent.history[-1] == C:
                self.coop_after_ab_count += 1
            else:
                self.def_after_ab_count += 1
            self.streak_needed = (
                np.floor(20.0 * self.def_after_ab_count / self.coop_after_ab_count) + 1
            )
            self.current_streak = 0
            return C

        proportion_agree = self.num_agreements / turn
        last_four_num = np.sum(self.last_four_agreements)
        if proportion_agree > 0.9 and last_four_num >= 4:
            return C
        elif proportion_agree >= 0.625 and last_four_num >= 2:
            return opponent.history[-1]
        return D

class Yamachi(Player):
    """
    Strategy submitted to Axelrod's second tournament by Brian Yamachi (K64R)
    and came in seventeenth in that tournament.

    The strategy keeps track of play history through a variable called
    `count_them_us_them`, which is a dict indexed by (X, Y, Z), where X is an
    opponent's move and Y and Z are the following moves by this player and the
    opponent, respectively.  Each turn, we look at our opponent's move two
    turns ago, call X, and our move last turn, call Y.  If (X, Y, C) has
    occurred more often (or as often) as (X, Y, D), then Cooperate.  Otherwise
    Defect.  [Note that this reflects likelihood of Cooperations or Defections
    in opponent's previous move; we don't update `count_them_us_them` with
    previous move until next turn.]

    Starting with the 41st turn, there's a possibility to override this
    behavior.  If `portion_defect` is between 45% and 55% (exclusive), then
    Defect, where `portion_defect` equals number of opponent defects plus 0.5
    divided by the turn number (indexed by 1).  When overriding this way, still
    record `count_them_us_them` as though the strategy didn't override.

    Names:

    - Yamachi: [Axelrod1980b]_
    """

    name = "Yamachi"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": False,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self) -> None:
        super().__init__()
        self.count_them_us_them = {
            (C, C, C): 0,
            (C, C, D): 0,
            (C, D, C): 0,
            (C, D, D): 0,
            (D, C, C): 0,
            (D, C, D): 0,
            (D, D, C): 0,
            (D, D, D): 0,
        }
        self.mod_history = list()  # type: List[Action]

    def try_return(self, to_return, opp_def):
        """
        Return `to_return`, unless the turn is greater than 40 AND
        `portion_defect` is between 45% and 55%.

        In this case, still record the history as `to_return` so that the
        modified behavior doesn't affect the calculation of `count_us_them_us`.
        """
        turn = len(self.history) + 1

        self.mod_history.append(to_return)

        # In later turns, check if the opponent is close to 50/50
        # If so, then override
        if turn > 40:
            portion_defect = (opp_def + 0.5) / turn
            if 0.45 < portion_defect and portion_defect < 0.55:
                return D

        return to_return

    def strategy(self, opponent: Player) -> Action:
        turn = len(self.history) + 1
        if turn == 1:
            return self.try_return(C, 0)

        us_last = self.mod_history[-1]
        them_two_ago, us_two_ago, them_three_ago = C, C, C
        if turn >= 3:
            them_two_ago = opponent.history[-2]
            us_two_ago = self.mod_history[-2]
        if turn >= 4:
            them_three_ago = opponent.history[-3]

        # Update history
        if turn >= 3:
            self.count_them_us_them[(them_three_ago, us_two_ago, them_two_ago)] += 1

        if (
            self.count_them_us_them[(them_two_ago, us_last, C)]
            >= self.count_them_us_them[(them_two_ago, us_last, D)]
        ):
            return self.try_return(C, opponent.defections)
        return self.try_return(D, opponent.defections)

class Colbert(FSMPlayer):
    """
    Strategy submitted to Axelrod's second tournament by William Colbert (K51R)
    and came in eighteenth in that tournament.

    In the first eight turns, this strategy Coopearates on all but the sixth
    turn, in which it Defects.  After that, the strategy responds to an
    opponent Cooperation with a single Cooperation, and responds to a Defection
    with a chain of responses:  Defect, Defect, Cooperate, Cooperate.  During
    this chain, the strategy ignores opponent's moves.

    Names:

    - Colbert: [Axelrod1980b]_
    """

    name = "Colbert"
    classifier = {
        "memory_depth": 4,
        "stochastic": False,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self) -> None:
        transitions = (
            (0, C, 1, C),
            (0, D, 1, C),  # First 8 turns are special
            (1, C, 2, C),
            (1, D, 2, C),
            (2, C, 3, C),
            (2, D, 3, C),
            (3, C, 4, C),
            (3, D, 4, C),
            (4, C, 5, D),
            (4, D, 5, D),  # Defect on 6th turn.
            (5, C, 6, C),
            (5, D, 6, C),
            (6, C, 7, C),
            (6, D, 7, C),
            (7, C, 7, C),
            (7, D, 8, D),
            (8, C, 9, D),
            (8, D, 9, D),
            (9, C, 10, C),
            (9, D, 10, C),
            (10, C, 7, C),
            (10, D, 7, C),
        )

        super().__init__(transitions=transitions, initial_state=0, initial_action=C)

class Mikkelson(FSMPlayer):
    """
    Strategy submitted to Axelrod's second tournament by Ray Mikkelson (K66R)
    and came in twentieth in that tournament.

    The strategy keeps track of a variable called `credit`, which determines if
    the strategy will Cooperate, in the sense that if `credit` is positive,
    then the strategy Cooperates.  `credit` is initialized to 7.  After the
    first turn, `credit` increments if the opponent Cooperated last turn, and
    decreases by two otherwise.  `credit` is capped above by 8 and below by -7.
    [`credit` is assessed as postive or negative, after increasing based on
    opponent's last turn.]

    If `credit` is non-positive within the first ten turns, then the strategy
    Defects and `credit` is set to 4.  If `credit` is non-positive later, then
    the strategy Defects if and only if (total # opponent Defections) / (turn#)
    is at least 15%.  [Turn # starts at 1.]

    Names:

    - Mikkelson: [Axelrod1980b]_
    """

    name = "Mikkelson"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": False,
        "makes_use_of": set(),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self) -> None:
        super().__init__()
        self.credit = 7

    def strategy(self, opponent: Player) -> Action:
        turn = len(self.history) + 1
        if turn == 1:
            return C

        if opponent.history[-1] == C:
            self.credit += 1
            if self.credit > 8:
                self.credit = 8
        else:
            self.credit -= 2
            if self.credit < -7:
                self.credit = -7

        if turn == 2:
            return C
        if self.credit > 0:
            return C
        if turn <= 10:
            self.credit = 4
            return D
        if opponent.defections / turn >= 0.15:
            return D
        return C

class Rowsam(Player):
    """
    Strategy submitted to Axelrod's second tournament by Glen Rowsam (K58R)
    and came in 21st in that tournament.

    The strategy starts in Normal mode, where it cooperates every turn.  Every
    six turns it checks the score per turn.  [Rather the score of all previous
    turns divided by the turn number, which will be one more than the number of
    turns scored.]  If this measure is less than 2.5 (the strategy is doing
    badly) and it increases `distrust_points`.  `distrust_points` is a variable
    that starts at 0; if it ever exceeds 6 points, the strategy will enter
    Defect mode and defect from then on.  It will increase `distrust_points`
    depending on the precise score per turn according to:

    - 5 points if score per turn is less than 1.0
    - 3 points if score per turn is less than 1.5, but at least 1.0
    - 2 points if score per turn is less than 2.0, but at least 1.5
    - 1 points if score per turn is less than 2.5, but at least 2.0

    If `distrust_points` are increased, then the strategy defects on that turn,
    then cooperates and defects on the next two turns.  [Unless
    `distrust_points` exceeds 6 points, then it will enter Defect mode
    immediately.]

    Every 18 turns in Normal mode, the strategy will decrement `distrust_score`
    if it's more than 3.  This represents a wearing off effect of distrust.

    Names:

    - Rowsam: [Axelrod1980b]_
    """

    name = "Rowsam"
    classifier = {
        "memory_depth": float("inf"),
        "stochastic": False,
        "makes_use_of": set("game"),
        "long_run_time": False,
        "inspects_source": False,
        "manipulates_source": False,
        "manipulates_state": False,
    }

    def __init__(self) -> None:
        super().__init__()
        self.mode = "Normal"
        self.distrust_points = 0
        self.current_score = 0
        self.opponent_score = 0

    def _score_last_round(self, opponent: Player):
        """Updates the scores for each player."""
        game = self.match_attributes["game"]
        last_round = (self.history[-1], opponent.history[-1])
        scores = game.score(last_round)
        self.current_score += scores[0]
        self.opponent_score += scores[1]

    def strategy(self, opponent: Player) -> Action:
        turn = len(self.history) + 1
        if turn > 1:
            self._score_last_round(opponent)

        if self.mode == "Defect":
            return D

        if self.mode == "Coop Def Cycle 1":
            self.mode = "Coop Def Cycle 2"
            return C

        if self.mode == "Coop Def Cycle 2":
            self.mode = "Normal"
            return D

        # Opportunity for distrust to cool off.
        if turn % 18 == 0:
            if self.distrust_points >= 3:
                self.distrust_points -= 1

        # In normal mode, only check for strategy updates every sixth turn.
        if turn % 6 != 0:
            return C

        points_per_turn = self.current_score / turn  # Off by one
        if points_per_turn < 1.0:
            self.distrust_points += 5
        elif points_per_turn < 1.5:
            self.distrust_points += 3
        elif points_per_turn < 2.0:
            self.distrust_points += 2
        elif points_per_turn < 2.5:
            self.distrust_points += 1
        else:
            # Continue Cooperating
            return C

        if self.distrust_points >= 7:
            self.mode = "Defect"
        else:
            # Def this time, then coop, then def.
            self.mode = "Coop Def Cycle 1"
        return D

max_forks_repo_name: kjurgielajtis/Axelrod


text:
from contextlib import contextmanager
from numpy import sin, cos, arctan2, arcsin, pi
from astropy.coordinates import (
    FunctionTransform, UnitSphericalRepresentation, SphericalRepresentation,
    AltAz, CIRS
)
import astropy.units as u

from .time import local_hour_angle, earth_rotation_angle
from .change_trafo import use_transformation

def is_unit_repr(coord):
    return (
        isinstance(coord.data, UnitSphericalRepresentation)
        or coord.cartesian.x.unit == u.one
    )

def cirs_to_altaz(ra, dec, time, location):
    '''
    Convert intermediate right ascension / declination to altitude / azimuth.
    Azimuth is measured from North (0°) through East (90°)
    '''

    # declare variables to match notation
    δ = dec
    ϕ = location.lat.rad
    h = local_hour_angle(ra, time, location)

    # precalculate terms we need more than once
    sinδ = sin(δ)
    cosδ = cos(δ)
    sinϕ = sin(ϕ)
    cosϕ = cos(ϕ)
    sin_h = sin(h)
    cos_h = cos(h)

    # divide line1 by line 2 of (7.16), p. 256
    az = arctan2(
        - sinδ * sin_h,
        sinδ * cosϕ - cosδ * cos_h * sinϕ
    )

    alt = arcsin(sinδ * sinϕ + cosδ * cos_h * cosϕ)

    return alt, az

def altaz_to_cirs(alt, az, time, location):
    '''
    Convert altitude / azimuth to intermediate right ascension / declination.
    Azimuth is measured from North (0°) through East (90°)
    '''

    # declare variables to match notation
    λ, ϕ, _ = location.to_geodetic('WGS84')
    λ = λ.to_value(u.rad)
    ϕ = ϕ.to_value(u.rad)

    # precalculate terms we need more than once
    sin_a = sin(alt)
    cos_a = cos(alt)
    sin_Az = sin(az)
    cos_Az = cos(az)
    sinϕ = sin(ϕ)
    cosϕ = cos(ϕ)

    # divide line1 by line 2 of (7.19), p. 257
    h = arctan2(
        - cos_a * sin_Az,
        sin_a * cosϕ - cos_a * cos_Az * sinϕ
    )

    dec_i = arcsin(sin_a * sinϕ + cos_a * cos_Az * cosϕ)
    ra_i = (earth_rotation_angle(time) + λ - h) % (2 * pi)

    return ra_i, dec_i

def _altaz_to_cirs(fromcoord, toframe):
    ra_i, dec_i = altaz_to_cirs(
        alt=fromcoord.alt.rad,
        az=fromcoord.az.rad,
        time=fromcoord.obstime,
        location=fromcoord.location,
    )

    ra_i = u.Quantity(ra_i, u.rad, copy=False)
    dec_i = u.Quantity(dec_i, u.rad, copy=False)

    if is_unit_repr(fromcoord):
        rep = UnitSphericalRepresentation(lon=ra_i, lat=dec_i)
    else:
        rep = SphericalRepresentation(
            lon=ra_i, lat=dec_i, distance=fromcoord.distance
        )

    return toframe.realize_frame(rep)

def _cirs_to_altaz(fromcoord, toframe):
    alt, az = cirs_to_altaz(
        ra=fromcoord.ra.rad,
        dec=fromcoord.dec.rad,
        time=fromcoord.obstime,
        location=fromcoord.location,
    )

    alt = u.Quantity(alt, u.rad, copy=False)
    az = u.Quantity(az, u.rad, copy=False)

    if is_unit_repr(fromcoord):
        rep = UnitSphericalRepresentation(lon=az, lat=alt)
    else:
        rep = SphericalRepresentation(
            lon=az, lat=alt, distance=fromcoord.distance
        )

    return toframe.realize_frame(rep)

transform_altaz_to_cirs = FunctionTransform(_altaz_to_cirs, AltAz, CIRS)
transform_cirs_to_altaz = FunctionTransform(_cirs_to_altaz, CIRS, AltAz)

@contextmanager
def no_polar_motion():
    '''
    Use approximate transforms for AltAz <-> CIRS that
    do not include polar motion.
    '''
    try:
        with use_transformation(CIRS, AltAz, transform_cirs_to_altaz):
            with use_transformation(AltAz, CIRS, transform_altaz_to_cirs):
                yield
    finally:
        pass

ext: py

reason: from numpy,import astropy,from astropy


text:
import numpy as np
import pandas as pd

from kf_lib.actors.fighter_factory import new_fighter, new_prize_fighter
from kf_lib.fighting.fight import AutoFight
from kf_lib.mechanics.experience import extract_features
from kf_lib.utils import rnd, rndint

np.random.seed(0)

feature_labels_str = 'lv1,lv2,lvrel,att1,att2,attrel,tech1,tech2,techrel,n1,n2,nrel,wp1,wp2,wprel,y'
feature_labels = feature_labels_str.split(',')

# todo refactor generate_data
def generate_data(examples=1000, min_lv=1, max_lv=20, max_n=8, group_fight_chance=0.5,
                  one_vs_many_subchance=0.5, tech_style_chance=0.75, wp_chance=0.1):
    data_file = open(
        os.path.join(
            'ml', f'ML_fight_data m={examples}, lv={min_lv}-{max_lv}, max_crowd={max_n}.csv'),
        'w', encoding='utf-8')
    data_file.write(feature_labels_str)
    for i in range(examples):
        if not i % 100:
            print(f'{i}/{examples}')
        if rnd() <= group_fight_chance:
            # group fight
            if rnd() <= one_vs_many_subchance:
                n_a = 1
            else:
                n_a = rndint(1, max_n)
            if rnd() <= tech_style_chance:
                fs1 = [new_fighter(lv=rndint(min_lv, max_lv)) for _ in range(n_a)]
            else:
                fs1 = [new_prize_fighter(lv=rndint(min_lv, max_lv)) for _ in range(n_a)]
            n_b = rndint(1, max_n)
            if rnd() <= tech_style_chance:
                fs2 = [new_fighter(lv=rndint(min_lv, max_lv)) for _ in range(n_b)]
            else:
                fs2 = [new_prize_fighter(lv=rndint(min_lv, max_lv)) for _ in range(n_b)]
            side_a = fs1
            side_b = fs2
        else:
            # mano a mano
            if rnd() <= tech_style_chance:
                f1 = new_fighter(lv=rndint(min_lv, max_lv))
            else:
                f1 = new_prize_fighter(lv=rndint(min_lv, max_lv))
            if rnd() <= tech_style_chance:
                f2 = new_fighter(lv=rndint(min_lv, max_lv))
            else:
                f2 = new_prize_fighter(lv=rndint(min_lv, max_lv))
            side_a = [f1]
            side_b = [f2]
        # arm some fighters
        for f in side_a + side_b:
            if rnd() <= wp_chance:
                f.arm()
        feat_vals = extract_features(side_a, side_b)
        f = AutoFight(side_a=side_a, side_b=side_b)
        feat_vals.append(float(f.win))
        data_file.write('\n' + ','.join([str(val) for val in feat_vals]))
    print(f'Successfully generated {examples} examples.')
    data_file.close()

# todo refactor learn_logreg
def learn_logreg(data_file, feature_list=None):
    from sklearn.linear_model import LogisticRegression
    if feature_list is None:
        features = feature_labels[:-1]
    else:
        features = feature_list
    df = pd.read_csv(data_file)
    x = df[features]
    y = df[feature_labels[-1]]

    from sklearn.model_selection import train_test_split
    train_x, test_x, train_y, test_y = train_test_split(x, y, random_state=1)

    print('initializing the LR model')
    print(features)
    model = LogisticRegression(solver='lbfgs', random_state=0, max_iter=1000)
    print(f'training on {len(train_x)} samples')
    model.fit(train_x, train_y)

    print('predicting')
    h = model.predict(test_x)

    from sklearn.metrics import classification_report
    print(classification_report(test_y, h))
    clf = 'LR'
    m = len(df)
    feats = len(features)
    report_file = open(
        os.path.join('ml', f'ML report {clf} m={m} n={feats}.txt'), 'w', encoding='utf-8')
    print(classification_report(test_y, h), file=report_file)

    # REFERENCE, DO NOT DELETE!
    # print('coefficients =', model.coef_)
    # print('intercept =', model.intercept_)
    # # new = np.array([[0.94, 0.8, 4, 0.17]])
    # new = np.array([[0.94, 0.8, 4, 2.17]])
    # my_new = [1, 0.94, 0.8, 4, 2.17]  # first is for intercept
    # my_coef = [-8.85045241, 2.42517891, 0.23488417, 0.28182145, 5.22587173,]  # 0=intercept
    # my_neg_coef = [-x for x in my_coef]
    #
    # print('positive outcome:')
    # z = sum([my_new[j] * my_coef[j] for j in range(len(my_new))])
    # print('z =', z)
    # pos = sigmoid(z)
    # print('sigmoid =', pos)
    #
    # print('negative outcome:')
    # z = sum([my_new[j] * -my_coef[j] for j in range(len(my_new))])
    # print('z =', z)
    # neg = sigmoid(z)
    # print('sigmoid =', neg)
    #
    # tot = pos + neg
    # print('my probs:', pos/tot, neg/tot)
    #
    # print('predict =', model.predict(new))
    # print('predict_proba =', model.predict_proba(new))
    # print('y =', 1)
    # REFERENCE, DO NOT DELETE!

    # save model
    coef_file = open(
        os.path.join('ml', f'ML LR model coef m={m} n={feats}.txt'), 'w', encoding='utf-8')
    print(features, file=coef_file)
    print('intercept:', model.intercept_, 'coefficients:', model.coef_, file=coef_file)
    coef_file.close()

# todo refactor learn_rfc
def learn_rfc(data_file, feature_list=None):
    from sklearn.ensemble import RandomForestClassifier
    if feature_list is None:
        features = feature_labels[:-1]
    else:
        features = feature_list
    df = pd.read_csv(data_file)
    x = df[features]
    y = df[feature_labels[-1]]

    from sklearn.model_selection import train_test_split
    train_x, test_x, train_y, test_y = train_test_split(x, y, random_state=1)

    print('initializing the RFC model')
    print(features)
    model = RandomForestClassifier(n_jobs=2, random_state=0)
    print(f'training on {len(train_x)} samples')
    model.fit(train_x, train_y)

    print('predicting')
    h = model.predict(test_x)

    # View a list of the features and their importance scores
    print('feature importance scores:')
    print(list(zip(train_x, model.feature_importances_)))

    from sklearn.metrics import classification_report
    print(classification_report(test_y, h))
    clf = 'RFC'
    m = len(df)
    feats = len(features)
    report_file = open(os.path.join('ml', f'ML report {clf} m={m} n={feats}.txt'), 'w',
                       encoding='utf-8')
    print(classification_report(test_y, h), file=report_file)

    # print(model.coef_)

max_stars_repo_name: AlexeyMalafeev/kfw-the-game

max_issues_repo_name: AlexeyMalafeev/kfw-the-game

max_forks_repo_name: AlexeyMalafeev/kfw-the-game

reason: import numpy


text:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=wrong-import-position

"""
Recursively search for events, truth, and recos .npy files, and aggregate into
a single numpy array.
"""

from __future__ import absolute_import, division, print_function

__all__ = [
    "EVENT_ID_FIELDS",
    "get_common_mask",
    "get_common_events",
    "concatenate_recos",
]

from argparse import ArgumentParser
from collections import  OrderedDict
try:
    from collections import Iterable
except ImportError:
    from collections.abc import Iterable
from glob import glob
from os import listdir, walk
from os.path import (
    abspath,
    dirname,
    expanduser,
    expandvars,
    isdir,
    isfile,
    join,
    splitext,
)
import sys

import numpy as np
from six import string_types
import numba

if __name__ == "__main__" and __package__ is None:
    RETRO_DIR = dirname(dirname(dirname(abspath(__file__))))
    if RETRO_DIR not in sys.path:
        sys.path.append(RETRO_DIR)
from retro.utils.misc import expand, join_struct_arrays, mkdir, nsort_key_func

EVENT_ID_FIELDS = [
    "run_id",
    "sub_run_id",
    "event_id",
    "sub_event_id",
    "start_time",
    "end_time",
]
"""Note that "start_time" and "end_time" are only necessary because poeple are
inconsiderate assholes, overwriting the *_id fields and making them no longer
unique (at least in MC)"""

@numba.jit(
    nopython=True,
    nogil=True,
    parallel=True,
    fastmath=False,
    error_model="numpy",
    cache=True,
)
def get_common_mask(event_ids, common_ids):
    """Create a mask for events IDs that are found in common set of event IDs.

    Parameters
    ----------
    event_ids, common_ids : numpy.arrays
        Arrays dtypes must contain `EVENT_ID_FIELDS` (and with sub-dtypes as
        found in the equality comparison in the below code).

    Returns
    -------
    common_mask : numpy.array of dtype numpy.bool8

    """
    common_mask = np.empty(shape=event_ids.shape, dtype=np.bool8)
    for idx in numba.prange(len(event_ids)):  # pylint: disable=not-an-iterable
        e_id = event_ids[idx]
        event_id_is_common = False
        for c_id in common_ids:
            if (
                e_id["run_id"] == c_id["run_id"]
                and e_id["sub_run_id"] == c_id["sub_run_id"]
                and e_id["event_id"] == c_id["event_id"]
                and e_id["sub_event_id"] == c_id["sub_event_id"]
                and e_id["start_time"]["utc_year"] == c_id["start_time"]["utc_year"]
                and e_id["start_time"]["utc_daq_time"] == c_id["start_time"]["utc_daq_time"]
                and e_id["end_time"]["utc_year"] == c_id["end_time"]["utc_year"]
                and e_id["end_time"]["utc_daq_time"] == c_id["end_time"]["utc_daq_time"]
            ):
                event_id_is_common = True
                break
        common_mask[idx] = event_id_is_common
    return common_mask

def get_common_events(arrays):
    """Get only those events from each array that are common to all arrays.

    Parameters
    ----------
    arrays : sequence of numpy.array

    Returns
    -------
    common_arrays : list of numpy.array

    """
    all_ids = []
    ids_dtype = None
    for array in arrays:
        ids = array[EVENT_ID_FIELDS]
        all_ids.append(ids)
        ids_dtype = ids.dtype

    all_ids_sets = [set(ids.tolist()) for ids in all_ids]

    # Enforce that IDs must be unique
    for array_num, (array, ids_set) in enumerate(zip(arrays, all_ids_sets)):
        if len(ids_set) != len(array):
            raise ValueError(
                "array {}: IDs are not unique! (len(ids) = {}) != (len(array) = {})".format(
                    array_num, len(ids_set), len(array)
                )
            )

    common_ids = np.array(sorted(set.intersection(*all_ids_sets)), dtype=ids_dtype)
    print("Number of events common to all arrays:", len(common_ids))

    common = []
    for array_num, (array, ids) in enumerate(zip(arrays, all_ids)):
        common_mask = get_common_mask(event_ids=ids, common_ids=common_ids)
        selected = array[common_mask]
        if len(selected) != len(common_ids):
            raise ValueError(
                "array {}: (len(selected) = {}) != (len(common_ids) = {})".format(
                    array_num, len(selected), len(common_ids)
                )
            )
        common.append(np.sort(selected, order=EVENT_ID_FIELDS))

    return common

def concatenate_recos(root_dirs, recos="all", allow_missing_recos=True):
    """Concatenate events, truth, and recos recursively found within `root_dirs`.

    Parameters
    ----------
    root_dirs : str or iterable thereof
    recos : str or iterable thereof, or None
        Specify "all" to load all recos found in first "events/recos" dir
        encountered. Specify None to load no recos.
    allow_missing_recos : bool

    Returns
    -------
    joined_concacatenated_array : numpy.array

    """
    orig_root_dirs = root_dirs
    if isinstance(root_dirs, string_types):
        root_dirs = [root_dirs]

    all_dirs = []
    for root_dir in root_dirs:
        all_dirs.extend(sorted(glob(root_dir), key=nsort_key_func))
    root_dirs = []
    for dpath in all_dirs:
        dpath = expanduser(expandvars(dpath))
        if not isdir(dpath):
            raise IOError('Directory does not exist: "{}"'.format(dpath))
        root_dirs.append(dpath)

    if not root_dirs:
        raise ValueError(
            "Found no directories; `root_dirs` = {}".format(orig_root_dirs)
        )

    if recos is None:
        expected_reco_names = []
    elif isinstance(recos, string_types):
        if recos == "all":
            expected_reco_names = None
        else:
            expected_reco_names = [recos]
    else:
        assert isinstance(recos, Iterable)
        expected_reco_names = list(recos)
        for reco in expected_reco_names:
            if reco == "all":
                assert len(expected_reco_names) == 1
                expected_reco_names = None

    if expected_reco_names:
        for reco_name in expected_reco_names:
            assert isinstance(reco_name, string_types), str(reco_name)

    sys.stdout.write("`recos` to extract: {}\n".format(expected_reco_names))

    paths_with_empty_events_file = []
    paths_with_nonempty_events_file = []
    all_events = []
    all_truths = []
    all_recos = []
    total_num_events = 0
    events_dtype = None
    has_truth = False
    all_found_reco_names = set()

    for root_dir in root_dirs:
        for dirpath, dirs, files in walk(root_dir, followlinks=True):
            # Force directory walking to be ordered in most logical way for our
            # files (we only need consistency, but nice to sort in a sensible
            # way). Note this must be an in-place sort. See ref.
            #   https://stackoverflow.com/a/6670926
            dirs.sort(key=nsort_key_func)

            if "events.npy" not in files:
                continue

            this_events = np.load(join(dirpath, "events.npy"))
            if len(this_events) == 0:
                paths_with_empty_events_file.append(dirpath)
                continue

            if (
                "L5_oscNext_bool" in this_events.dtype.names
                and np.count_nonzero(this_events["L5_oscNext_bool"]) == 0
            ):
                paths_with_empty_events_file.append(dirpath)
                continue

            paths_with_nonempty_events_file.append(dirpath)

            if events_dtype is None:
                events_dtype = this_events.dtype
            else:
                if this_events.dtype != events_dtype:
                    raise TypeError(
                        '"{}": events.dtype should be\n{}\nbut dtype found'
                        "is\n{}".format(
                            dirpath, events_dtype, this_events.dtype
                        )
                    )

            this_truths = None
            if "truth.npy" in files:
                this_truths = np.load(join(dirpath, "truth.npy"))
                has_truth = True
                if len(this_truths) != len(this_events):
                    raise ValueError(
                        "len(this_truths) = {} != len(this_events) = {}, dir {}"
                        .format(len(this_truths), len(this_events), dirpath)
                    )
            elif has_truth:
                raise IOError(
                    '"truth.npy" exists in other directories but not in "{}"'.format(
                        dirpath
                    )
                )

            if expected_reco_names is None:
                reco_fnames = sorted(listdir(join(dirpath, "recos")))
                this_expected_reco_names = [splitext(reco_fname)[0] for reco_fname in reco_fnames]
            else:
                this_expected_reco_names = expected_reco_names
            all_found_reco_names.update(this_expected_reco_names)

            missing_recos = []
            if this_expected_reco_names:
                this_recos = OrderedDict()
                for reco_name in this_expected_reco_names:
                    fpath = join(dirpath, "recos", reco_name + ".npy")
                    if not isfile(fpath):
                        if not allow_missing_recos:
                            raise ValueError(
                                'Missing reco "{}" in dir "{}"'.format(
                                    reco_name, dirpath
                                )
                            )
                        missing_recos.append(reco_name)
                        break
                    reco = np.load(fpath)
                    if len(reco) != len(this_events):
                        raise ValueError(
                            'reco "{}" len = {}, events len = {} in dir "{}"'.format(
                                reco_name, len(reco), len(this_events), dirpath
                            )
                        )
                    this_recos[reco_name] = reco
            else:
                this_recos = None

            if missing_recos:
                print(
                    'Missing recos {} in dir "{}", skipping'.format(
                        missing_recos, dirpath
                    )
                )

            all_events.append(this_events)
            all_truths.append(this_truths)
            all_recos.append(this_recos)
            total_num_events += len(this_events)

    # -- Loop through, validating the above -- #

    all_dirs_concatenated = []
    all_events_to_process = []
    all_truths_to_process = []
    all_recos_to_process = OrderedDict()

    for dirpath, this_events, this_truths, this_recos in zip(
        paths_with_nonempty_events_file,
        all_events,
        all_truths,
        all_recos,
    ):
        if len(all_found_reco_names) > 0:
            if this_recos is None or set(this_recos.keys()) != all_found_reco_names:
                if allow_missing_recos:
                    continue
                else:
                    raise ValueError('"{}"'.format(dirpath))

            for reco_name, reco_vals in this_recos.items():
                if reco_name not in all_recos_to_process:
                    all_recos_to_process[reco_name] = []
                all_recos_to_process[reco_name].append(reco_vals)

        if has_truth:
            if this_truths is None:
                raise ValueError('missing truth in dir "{}"'.format(dirpath))
            else:
                all_truths_to_process.append(this_truths)

        all_events_to_process.append(this_events)
        all_dirs_concatenated.append(dirpath)

    print(
        "Paths with empty events files: {:d}, paths with non-empty events"
        " files: {:d}, total: {:d}".format(
            len(paths_with_empty_events_file),
            len(paths_with_nonempty_events_file),
            len(paths_with_empty_events_file) + len(paths_with_nonempty_events_file),
        )
    )
    print(
        "Paths with non-empty events files skipped due to missing reco(s):"
        " {:d}".format(
            len(paths_with_nonempty_events_file) - len(all_dirs_concatenated)
        )
    )

    if len(all_events_to_process) == 0:
        raise ValueError(
            "Found no events to concatenate recursively from path(s) {}".format(
                root_dirs
            )
        )

    # Create a single array with all extracted info; first-level dtype names
    # are all dtype names from `events` plus "truth" and the names of each reco

    events = np.concatenate(all_events_to_process)
    if has_truth:
        truth = np.concatenate(all_truths_to_process)
        # Re-cast such that dtype is ["truth"][key_n] instead of just [key_n]
        truth = truth.view(dtype=np.dtype([("truth", truth.dtype)]))
    else:
        truth = None

    if len(all_found_reco_names) == 0:
        recos = None
    else:
        recos = []
        for reco_name, reco_vals in all_recos_to_process.items():
            reco_vals = np.concatenate(reco_vals)
            # Re-cast such that dtype is ["truth"][key_n] instead of just [key_n]
            reco_vals = reco_vals.view(dtype=np.dtype([(reco_name, reco_vals.dtype)]))
            recos.append(reco_vals)

    to_join = [events]
    if truth is not None:
        to_join.append(truth)
    if recos is not None:
        to_join.extend(recos)

    joined_concacatenated_array = join_struct_arrays(to_join)

    return joined_concacatenated_array

def concatenate_recos_and_save(outfile, **kwargs):
    """Concatenate recos and save to a file.

    Parameters
    ----------
    outfile : str
    **kwargs
        Arguments passed to `concatenate_recos`

    """
    outfile = expand(outfile)
    out_array = concatenate_recos(**kwargs)
    outdir = dirname(outfile)
    if not isdir(outdir):
        mkdir(outdir)
    np.save(outfile, out_array)
    sys.stdout.write('Saved concatenated array to "{}"\n'.format(outfile))

def main():
    """Script interface to `concatenate_recos_and_save` function"""
    parser = ArgumentParser(
        description="""Concatenate events, truth, and reco(s) from one or more
        directories (searching recursively) into a single numpy array"""
    )
    parser.add_argument(
        "-d",
        "--root-dirs",
        required=True,
        nargs="+",
        help="""Directories in which to search for events, truth, and
        associated recos""",
    )
    parser.add_argument(
        "--recos",
        nargs="+",
        default=None,
        help="""Specify reco names to retrieve, or specify "all" to retrieve
        all recos present (at least those found in the first events/recos
        directory encountered); """,
    )
    parser.add_argument(
        "--allow-missing-recos",
        action="store_true",
        help="""Allow recos explicitly specified by
            --recos <reco_name0> <reco_name1> ..
        to be absent in all found directories (if present in some but absent in
        others, an error will still result).""",
    )
    parser.add_argument(
        "--outfile",
        required=True,
        help="""Results will be written to this ".npy" file.""",
    )
    kwargs = vars(parser.parse_args())
    concatenate_recos_and_save(**kwargs)

if __name__ == "__main__":
    main()

reason: import numpy,import numba