class Prisoner:
    
    def __init__(self, label: str, strategy):
        self.label = label
        self.score = 0
        self.strategy = strategy
        
    def decide(self, history: list) -> tuple:
        """
        strategy is a function that returns a decision in the form
        of a tuple containing the prisoner's label and a 
        string indicating the choice
        """
        return self.strategy(self, history)


"""
Explanation: Prisoner's Dilemma
This is an implementation of the Prisoner's dilemma using Python.
First thing we do is define what a Prisoner is.
End of explanation
"""


def always_betray(p: Prisoner, history: list):
    return (p.label, "BETRAY")

def always_silent(p: Prisoner, history: list):
    return (p.label, "SILENT")

"""
Explanation: Prisoners are essentially containers for strategies and scores associated with those strategies. As the dilemma continues for a given number of turns, the scores will change according to these rules:
A/B | B Silent | B Betrays
--|--
A Silent | -1 /-1 | -3 / 0
A Betrays |0 / -3 | -2 / -2
Scores
The way this works, our scores will always be negative. Think about this as how many years the Prisoner has added to its sentence. The closer to 0, the better the Prisoner has done in the game.
Strategies
The Prisoner needs a strategy. For ease, we're going to abstract strategies from the Prisoner itself, so we can make a bunch, and then create Prisoners with whichever strategy we like.
This approach is solid, since the shape of each strategy will be the same: a function that returns a tuple containing the Prisoner's label, and the move: either "BETRAY" or "SILENT".
Now let's make some strategies. The easiest, of course, is to always do the same thing. Let's do those:
End of explanation
"""


def scorched_earth(p: Prisoner, history):
    """
    If you cross me JUST ONCE, it's all-out war from here on out.
    """
    
    # First, filter history for just our opponent's betrayals.
    o_hist = list(filter(lambda x: x[0] != p.label and x[1] == "BETRAY"))
    
    # If there are any betrayals, it's on.
    if len(o_hist) > 0:
        return (p.label, "BETRAY")
    
    return (p.label, "SILENT")

def vengeful(p: Prisoner, history):
    """
    Betrays on a prior betrayal, but keeps it peaceful otherwise
    """
    
    # First, filter history for just our opponent's.
    o_hist = list(filter(lambda x: x[0] != p.label, history))
    
    # if the last move was to betray, get 'im back
    if len(o_hist) > 0:
        if o_hist[-1][1] == "BETRAY":
            return (p.label, "BETRAY")
        
        return (p.label, "SILENT")
    
    # Otherwise, keep it peaceful
    return (p.label, "SILENT")

def grudge(p: Prisoner, history):
    """
    If you've crossed me in the last 5 turns, I'll betray you
    """
    
    o_hist = list(filter(lambda x: x[0] != p.label, history))
    recent_o_hist = o_list[len(o_hist) - 5 :: ]
    recent_betrayals = list(filter(lambda x: x[1] == "BETRAY", recent_o_hist))
    
    if len(recent_betrayals) > 0:
        return (p.label, "BETRAY")
    
    return (p.label, "SILENT")
    

import random

def coin_toss(p: Prisoner, history):
    """
    The true madman's strategy
    """
    return (p.label, random.choice(["SILENT","BETRAY"]))

"""
Explanation: But you see of course, that these strategy functions can also look at history. We haven't seen the history yet, but once we have a place to put it, it'll be a list of every event in the "game." We can filter down to see only events from the other prisoner, and make choices based on the pattern.
For instance, perhaps we want to test a strategy that is nice until the other player is not, at which point, it's "No more Mr. Nice Prisoner," and they start betraying every turn. Or perhaps we'll betray on a betrayal, but maintain peace otherwise.
Or maybe we'll flip a coin and let fate decide.
End of explanation
"""


prisoner_a = Prisoner("Prisoner A", always_silent)
prisoner_b = Prisoner("Prisoner B", always_silent)

"""
Explanation: With that done, it's time to make us some prisoners. Let's make them friendly to start.
End of explanation
"""


class Prison:
    
    def __init__(self, prisoner_a, prisoner_b):
        self.prisoner_a = prisoner_a
        self.prisoner_b = prisoner_b
        self.history = []
        
    def turn(self):
        """
        Run a single turn of the dilemma, making prisoners have to employ
        their strategy. Also, adjust the scores accordingly.
        """
        choice_a = self.prisoner_a.decide(self.history)
        choice_b = self.prisoner_b.decide(self.history)
        
        # Time to evaluate the results
        if choice_a[1] == "SILENT" and choice_b[1] == "SILENT":
            self.prisoner_a.score -= 1
            self.prisoner_b.score -= 1
        elif choice_a[1] == "SILENT" and choice_b[1] == "BETRAY":
            self.prisoner_a.score -= 3
        elif choice_a[1] == "BETRAY" and choice_b[1] == "SILENT":
            self.prisoner_b.score -= 3
        else:
            self.prisoner_a.score -= 2
            self.prisoner_b.score -= 2
        
        #Then add the choices to history        
        self.history.append(choice_a)
        self.history.append(choice_b)
    
    def run(self, turns: int) -> list:
        """
        Actually run the simulatio
        """
        
        # Resets scores just in case
        self.prisoner_a.score = 0
        self.prisoner_b.score = 0
        
        for i in range(turns):
            self.turn()
            
        return self.history
    

"""
Explanation: But of course, Prisoners are not that useful without a scenario. Or, more poetically, a Prison.
End of explanation
"""


from pprint import pprint # Pretty Printer

def print_results(prison):
    """
    Prettily print the results from a prison.
    """
    score_a = prison.prisoner_a.score
    score_b = prison.prisoner_b.score
    pprint(prison.history)
    print(prison.prisoner_a.label + ": " + str(score_a))
    print(prison.prisoner_b.label + ": " + str(score_b))

prison = Prison(prisoner_a, prisoner_b)
prison.run(10)

print_results(prison)

"""
Explanation: I realize there's some repetition in that code. However, since we really only deal with 2 prisoners at a time, I have broken the "Don't Repeat Yourself" (DRY) rule in the service of clarity.
Let's get the prisoner's dilemma-ing:
End of explanation
"""


p1 = Prisoner("Vengeful prisoner", vengeful)
p2 = Prisoner("Friendly prisoner", always_silent)
prison = Prison(p1, p2)
prison.run(10)
print_results(prison)


"""
Explanation: If everyone cooperates the whole time, the amount of total years added across both Prisoners is minimized.
But generally, one or the other decides to take some advantage for themselves.
Let's try some other strategies. Let's make a vengeful prisoner go against a (naively) friendly one:
End of explanation
"""


p1 = Prisoner("Vengeful prisoner", vengeful)
p2 = Prisoner("Insane prisoner", coin_toss)
prison = Prison(p1, p2)
prison.run(10)
print_results(prison)

"""
Explanation: Okay, so not a lot of change there—obviously, since the friendly prisoner isn't going to trigger the vengeance. Let's give that prisoner something to get angry about. We'll use the coin_toss strategy.
Two-Face would approve:
End of explanation
"""


def irritable(p: Prisoner, history):
    """
    Predictably irritable strategy
    """
    
    # 5 rounds or fewer, stay silent. Otherwise, betray.
    if len(history) <= 10:
        return (p.label, "SILENT")
    
    return (p.label, "BETRAY")

"""
Explanation: Obviously a coin toss doesn't give us reliable results. Let's create a predictable strategy that always betrays after 5 rounds:
End of explanation
"""


p1 = Prisoner("Vengeful prisoner", vengeful)
p2 = Prisoner("Irritable prisoner", irritable)
prison = Prison(p1, p2)
prison.run(10)
print_results(prison)

"""
Explanation: So with that, let's pit an irritable prisoner against a vengeful one.
End of explanation
"""


def my_strategy(p: Prisoner, history):
    """
    Implement your own strategy here!
    """
    
    return (p.label, "BETRAY")

# Choose whatever strategies you like
p1 = Prisoner("Vengeful prisoner", vengeful)
p2 = Prisoner("Insane prisoner", my_strategy)
prison = Prison(p1, p2)
prison.run(10)
print_results(prison)

"""
Explanation: The Irritable Prisoner will continue to hold its score advantage over the Vengeful prisoner—once betrayal starts, the first person to attempt to return to silence loses.
Despite the Irritable Prisoner beating the Vengeful Prisoner this time, Vengeful (also known as "Tit-for-tat" is widely considered the most effective strategy.
But effective doesn't mean fun. Write your own strategies below and experiment!
End of explanation
"""


