#!/usr/bin/python

# PrisonersDilemma.py
# Copyright (C) Tobias Hermann 2011 <daiw@gmx.net>
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.


import random
import copy
import itertools


# faster than random.randint(lower ,upper),
# because random.random() is C
# and random.randint is pure Python
def random_int(lower, upper):
    """Return a random integer N such that a <= N <= b."""
    return int(lower + (1 + upper - lower) * random.random())

def invert_action(action):
    """Action.cooperate-Action.>defect; Action.defect->Action.cooperate"""
    if action == Action.cooperate:
        return Action.defect
    else:
        return Action.cooperate

class Action:
    """An action that can be performed by a player"""
    cooperate = 1
    defect = 2

class Player:
    """Base class for player types"""
    def play(self):
        raise NotImplementedError
    def process_partners_action(self, a):
        self._others_last_action = a


class NicePlayer(Player):
    """An always cooperating player"""
    def play(self):
        return Action.cooperate


class MeanPlayer(Player):
    """An always defecting player"""
    def play(self):
        return Action.defect


class SpitePlayer(Player):
    """A never forgiving player"""
    _otherDidDefect = False
    def play(self):
        if self._otherDidDefect:
            return Action.defect
        return Action.cooperate
    def process_partners_action(self, a):
        if a == Action.defect:
            self._otherDidDefect = True


class PerKind2Player(Player):
    """A CCD player"""
    _counter = 2
    def play(self):
        ++self._counter
        if self._counter > 2:
            self._counter = 0
        if self._counter == 2:
            return Action.defect
        else:
            return Action.cooperate


class PerNasty3Player(Player):
    """A DDDC player"""
    _counter = 3
    def play(self):
        ++self._counter
        if self._counter > 3:
            self._counter = 0
        if self._counter == 3:
            return Action.cooperate
        else:
            return Action.defect


class RandomPlayer(Player):
    """A pure random player"""
    def play(self):
        return random.choice([Action.cooperate, Action.defect])


class LikesMirrorsPlayer(Player):
    """A player who cooperates when both players did the same action
    in the last round. Else defects.
    In principle this is an immediatly forgiving TFT player"""
    _others_last_action = Action.cooperate
    _own_last_action = Action.cooperate
    def play(self):
        if self._others_last_action == self._own_last_action:
            self._own_last_action = Action.cooperate
        else:
            self._own_last_action = Action.defect
        return self._own_last_action


class AlternatingPlayer(Player):
    """A CDCDCD player."""
    _own_last_action = Action.defect
    def play(self):
        self._own_last_action = invert_action(self._own_last_action)
        return self._own_last_action


class OpponentInvertingPlayer(Player):
    """A player that does the opposite
    of what the opponent did the round before."""
    _others_last_action = Action.cooperate
    def play(self):
        return invert_action(self._others_last_action)


class WinCooperateLoseDefect(Player):
    """A player who cooperates when the last time was a success. Else defects.
    In principle this in a immediatly forgiving TFT player
    with one round delay before forgiving"""
    _others_last_action = Action.cooperate
    _own_last_action = Action.cooperate
    def play(self):
        if ((self._others_last_action == Action.cooperate and
                    self._own_last_action == Action.defect) or
                (self._others_last_action == Action.cooperate and
                    self._own_last_action == Action.cooperate)):
            self._own_last_action = Action.cooperate
        else:
            self._own_last_action = Action.defect
        return self._own_last_action


class WinStayLoseSwitch(Player):
    """A player who cooperates when the last time was a success.
    Else defects."""
    _others_last_action = Action.cooperate
    _own_last_action = Action.cooperate
    def play(self):
        if ((self._others_last_action == Action.defect and
                    self._own_last_action == Action.defect) or
                (self._others_last_action == Action.cooperate and
                    self._own_last_action == Action.cooperate)):
            self._own_last_action = invert_action(self._own_last_action)
        return self._own_last_action


class TFTPlayer(Player):
    """A Tit For Tat player"""
    _others_last_action = Action.cooperate
    def play(self):
        return self._others_last_action


class GoByMajority(Player):
    """A player that only defects when the other
    defected more often than he cooperated"""
    othersCooperateCount = 0
    othersDefectCount = 0
    def play(self):
        if self.othersDefectCount > self.othersCooperateCount:
            return Action.defect
        return Action.cooperate
    def process_partners_action(self, a):
        if a == Action.defect:
            ++self.othersDefectCount
        else:
            ++self.othersCooperateCount


class MistrustPlayer(Player):
    """A Tit For Tat player that defects in the first round."""
    _others_last_action = Action.defect
    def play(self):
        return self._others_last_action


class RemorsefulProber(Player):
    """A Tit For Tat player that probes and shows remorse"""
    def __init__(self, oneDivFreq):
        self._oneDFreq = oneDivFreq
    _others_last_action = Action.cooperate
    _probed = False
    def play(self):
        if (not self._probed and
                self._others_last_action == Action.cooperate and
                random_int(0, self._oneDFreq) == 0):
            self._probed = True
            return Action.defect
        if self._probed and self._others_last_action == Action.defect:
            return Action.cooperate
        return self._others_last_action


class TFTTPlayer(Player):
    """A Tit For Two Tats player"""
    _others_last_action = Action.cooperate
    def play(self):
        return self._others_last_action
    def process_partners_action(self, a):
        if self._others_last_action == Action.defect:
            return random.choice([Action.cooperate, Action.defect])
        self._others_last_action = a


class FTFTPlayer(Player):
    """A Forgiving Tit Fot Tat player"""
    def __init__(self, oneDivFreq):
        self._oneDFreq = oneDivFreq
    _others_last_action = Action.cooperate
    def play(self):
        if random_int(0, self._oneDFreq) == 0:
            _others_last_action = Action.cooperate
        return self._others_last_action


def create_player_from_string(player_string):
    """Creates a list with three elements:
        player_string, newly created player object, win_count = 0"""
    return [player_string, eval(player_string), 0]


def create_players_from_strings(player_strings):
    """Creates a list with instances of players from strings"""
    players = []
    for player_string in player_strings:
        players.append(create_player_from_string(player_string))
    return players


def play_generation(players):
    """Plays the tournament with one generation"""
    players_sums = copy.deepcopy(players)
    for pairs in list(itertools.product(players, repeat=2)):
        pA = pairs[0]
        pB = pairs[1]
        for game in range(2):
            a = copy.deepcopy(pA)
            b = copy.deepcopy(pB)
            for round in range(40):
                aAction = a[1].play()
                bAction = b[1].play()
                if aAction == Action.cooperate and bAction == Action.cooperate:
                    a[2] += 3
                    b[2] += 3
                elif aAction == Action.cooperate and bAction == Action.defect:
                    a[2] += 0
                    b[2] += 5
                elif aAction == Action.defect and bAction == Action.cooperate:
                    a[2] += 5
                    b[2] += 0
                elif aAction == Action.defect and bAction == Action.defect:
                    a[2] += 1
                    b[2] += 1
                if random_int(0, 30) == 0:
                    aAction = invert_action(aAction)
                if random_int(0, 30) == 0:
                    bAction = invert_action(bAction)
                a[1].process_partners_action(bAction)
                b[1].process_partners_action(aAction)
            players_sums[players.index(pA)][2] += a[2]
            players_sums[players.index(pB)][2] += b[2]
    return sorted(players_sums, key=lambda player: player[2], reverse=True)


def prisoners_dilemma_tournament():
    """Runs a tournament and removes the lowest scorer in each generation."""
    random.seed()
    player_strings = [
        'RandomPlayer()',
        'NicePlayer()',
        'MeanPlayer()',
        'AlternatingPlayer()',
        'LikesMirrorsPlayer()',
        'OpponentInvertingPlayer()',
        'PerKind2Player()',
        'PerNasty3Player()',
        'TFTPlayer()',
        'TFTTPlayer()',
        'FTFTPlayer(10)',
        'SpitePlayer()',
        'WinCooperateLoseDefect()',
        'WinStayLoseSwitch()',
        'RemorsefulProber(10)',
        'MistrustPlayer()',
        'GoByMajority()'
    ]

    player_strings *= 2
    players = create_players_from_strings(player_strings)
    while len(players) > 1:
        sorted_players_sums = play_generation(players)
        max_player_name_length = 0;
        for player in sorted_players_sums:
            max_player_name_length = max(max_player_name_length,
                                         len(player[0]))
        for sums in sorted_players_sums:
            print(sums[0].ljust(max_player_name_length), sums[2])
        player_strings = list(player[0] for player in sorted_players_sums)
        if len(players) > 1:
            print('---\nremoving', player_strings[-1], '\n---')
        del player_strings[-1]
        players = create_players_from_strings(player_strings)
    print('Winner: %s' % players[0][0])


if __name__ == "__main__":
    prisoners_dilemma_tournament()
