'''
Author: Casey S. Schroeder
Copyright (c) Casey S. Schroeder

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 
OR OTHER DEALINGS IN THE SOFTWARE.
'''


import random
import bisect
import time
class exploit(object):
    '''
        Dispersion of exploit is modified by the cynicism of the players in the game and obviousness of exploit.
        Cynicism increases/decreases per witness to this and other exploit's use.
        mu and sigma of the benefit of a given exploit's use are params to a normal distribution.
        
        Assume that an exploit cannot be used unless it is known - there is no accidental use - but has a discovery rate 
        which makes it known independent of use.
        
        Assume that all parameters are known to those who know of the exploit
    '''
    def __init__(self, name, zero_use_discovery_rate, obviousness_of_use, utility_mu, utility_sigma):
        ''' zero_use_discovery_rate is rate at which an exploit is discovered independent of use by others
            obviousness_of_use degree to which exploit's use is apparent to others; those who know of it or those who don't and learn 
            mu and sigma are parameters into a normal distribution for sampling the utility of the exploits use.
        '''
        self.name = name
        self.zero_use_discovery_rate = zero_use_discovery_rate 
        self.obviousness_of_use = obviousness_of_use
        self.utility_mu = utility_mu 
        self.utility_sigma = utility_sigma

class player(object):
    '''
       Currently does not include skill attributes, which could be modeled to an arbitrary - and unwieldy - precision
       Utility balance is the utility which the player has available; updated by utility payouts from rankings in games played.
       Cynicism is the measure of a player's suspicion of exploit use, in general, increasing both forms of exploit discovery 
                (zero use and observational).  Cynicysm itself increases with observed exploit use.
       cynicism_update_factor is used to determine how quickly cynicism grows per observation
       initial_rate is the initial rate of use of any given exploit
       exploits is dict of {game_name : {exploit_name: use_rate} }
       acquired_exploit is time first exploit is acquired
       acquired_exploits is dict of when a given exploit is acquired {exploit_name: time}
       utility_at_acquired is utility of player at time first exploit is acquired
       utilties_at_acquired is dict of utility of player when an exploit is first acquired {exploit_name: utility}
    '''
    def __init__(self,name,utility_balance,initial_rate,cynicism,cynicism_update_factor):
        self.name=name
        self.utility_balance=utility_balance 
        self.cynicism = cynicism 
        self.cynicism_update_factor = cynicism_update_factor
        self.initial_rate = initial_rate
        self.exploits={} 
        self.alive=True
        self.games_played=0
        self.acquired_exploit=-1
        self.acquired_exploits = {}
        self.utility_at_acquired=-1
        self.utilites_at_acquired = {}
        
    def update_cynicism(self,count):
        '''update function for cynicism'''
        self.cynicism += (1-self.cynicism)*self.cynicism_update_factor*count

class game(object):
    '''
        A game is a weighted lottery.  The weights of the lottery are determined by a distribution of 'shares'.
        Shares are distributed equally, but subject to unequal distribution given the use of exploits.  Payouts 
        are distributed in equal intervals between payout_max and payout_min according to order selected by lottery.
    '''
    def __init__(self,name,payout_max,payout_min,exploits,num_players,cost):
        ''''''
        self.name = name
        self.payout_max=payout_max 
        self.payout_min=payout_min
        self.exploits=exploits
        self.num_players=num_players
        self.cost=cost #in units of utility

    def exploit_dispersion(self,players,exploits_used,all_exploits,time):
        '''distributes knowledge of exploits per observation of use or discovery'''
        for p in players:
            count = 0
            for pname, e in exploits_used:
                if p.name == pname: continue #players own use
                if random.uniform(0,1) < all_exploits[e].obviousness_of_use * p.cynicism:
                    count += 1
                    if not e in p.exploits[self.name].keys():
                        p.exploits[self.name][e] = p.initial_rate
                        p.acquired_exploit=time
                        p.utility_at_acquired=p.utility_balance
            p.update_cynicism(count)

        for e in all_exploits.keys():
            for p in players:
                if not e in p.exploits[self.name].keys():
                    if random.uniform(0,1) < all_exploits[e].zero_use_discovery_rate * p.cynicism:
                        p.exploits[self.name][e] = p.initial_rate
                        p.acquired_exploit=time
                        p.utility_at_acquired=p.utility_balance

    def run(self, players, time):
        '''determines the result of a single game given players and the round (or time) at which this game takes place. '''
        if len(players) != self.num_players:
            raise Exception("Wrong number of players for game.")
        exploits_used=[]
        share_sum=0
        player_shares=[]
        count=0
        player_order=[]
        counted={}
        share_list=[]
        exploits={}
        for p in players:
            if not p.alive:
                raise Exception("Player is dead.")
            if p.utility_balance - self.cost < 0:
                raise Exception("Player does not have enough utility to play.")
            p.utility_balance -= self.cost
            p.games_played += 1
            shares=1
            try:
                exploits=p.exploits[self.name]
            except:
                p.exploits[self.name]={}
                exploits={}
            for e in exploits.keys():
                if random.uniform(0,1)<exploits[e] * p.cynicism:
                    exploits_used.append((p.name,e))
                    shares+=max(0,random.gauss(self.exploits[e].utility_mu,self.exploits[e].utility_sigma))
            share_list.append(shares)
        while players:
            share_sum=0
            player_shares=[]
            for s in share_list:
                share_sum+=s
                player_shares.append(share_sum)
            i=bisect.bisect(player_shares,random.uniform(0,share_sum))
            share_list.pop(i)
            p=players.pop(i)
            player_order.append(p)
        payout_range = self.payout_max-self.payout_min
        payout_inc = payout_range/(self.num_players-1)
        payout = payout_max
        for p in player_order:
            p.utility_balance+=payout
            if p.utility_balance < 1: p.alive=False
            payout-=payout_inc
        self.exploit_dispersion(player_order,exploits_used,self.exploits,time)

class simulation():
    def __init__(self, players, games, num_players_per_game):
        self.players=players
        self.games=games
        self.num_players_per_game = num_players_per_game

    def sim(self, num_rounds):
        dead=[]
        for k in xrange(num_rounds):
            if [p for p in self.players if not p.alive]:
                dead.append((k, [p for p in self.players if not p.alive]))
            self.players = [p for p in self.players if p.alive]
            random.shuffle(self.players)
            if len(self.players) <= 1:
                break
            for i in xrange(len(self.players)/self.num_players_per_game):
                if i*self.num_players_per_game < len(self.players):
                    self.games[0].run(self.players[i*self.num_players_per_game:(i+1)*self.num_players_per_game],k)
                else:
                    pass
        return dead, self.players

if __name__ == "__main__":
    num_trials=100
    increment=.2
    params=[]
    data=[]
    #create parameters for different runs using increment
    for iur in xrange(1,5):
        for ic in xrange(1,5):
            for cuf in xrange(1,5):
                params.append([iur*increment,ic*increment,cuf*increment])

    print time.strftime("%H:%M:%S")
    for param in params:
        alive_total=0
        dead_total=0
        dead_utility_at_acquire=0
        dead_time_at_acquire=0
        alive_utility_at_acquire=0
        alive_time_at_acquire=0
        alive_count=0
        dead_count=0
        #per parameter group, loop over number of trials.
        for x in xrange(num_trials):
            '''parameters for players and games'''
            num_games=1
            num_players=100
            num_players_per_game=2
            initial_use_rate = param[0]
            initial_cynicism = param[1]
            cynicism_update_factor = param[2]
            utility_per_player = 5
            payout_max = 2
            payout_min = 0
            cost_per_play = 1
            games=[]
            players=[]
            for j in range(num_players):
                players.append(player(j,utility_per_player,initial_use_rate,initial_cynicism,cynicism_update_factor))
            for i in xrange(num_games):
                games.append(game(i,payout_max,payout_min,{str(i)+"_exploit":exploit(str(i)+"_exploit",.01,.05,2,.5)},num_players_per_game,cost_per_play))
            '''parameter for simulation'''
            num_rounds=5000
            s=simulation(players,games,num_players_per_game)
            dead, players = s.sim(num_rounds)


            #'d' for dead
            d=0
            d2=0
            dcount=0
            no_exploit=0
            rounds=[]
            utility_at_acquire=[]
            time_of_acquire=[]
            tot_dead=0
            for t,l in dead:
                for x in l:
                    if x.acquired_exploit >= 0:
                        d+= x.acquired_exploit
                        d2+=x.utility_at_acquired
                        dcount+=1
                        if True: #t > num_rounds*.05:
                            rounds.append(t)
                            utility_at_acquire.append(x.utility_at_acquired)
                            time_of_acquire.append(x.acquired_exploit)
                    else:
                        no_exploit+=1

            dead_utility_at_acquire+=d2/dcount
            dead_time_at_acquire+=d/dcount
            dead_count+=dcount
            
            
            #'a' for 'alive'
            a=0
            a2=0
            acount=0
            no_exploit=0
            for p in players:
                if p.acquired_exploit >= 0:
                    a+= p.acquired_exploit
                    a2+=p.utility_at_acquired
                    rounds.append(num_rounds)
                    utility_at_acquire.append(p.utility_at_acquired)
                    time_of_acquire.append(p.acquired_exploit)
                    acount+=1
                else:
                    no_exploit+=1

            alive_utility_at_acquire+=a2/acount
            alive_time_at_acquire+=a/acount
            alive_count+=acount

        data.append([num_trials, param, [1.0*dead_count/num_trials, 1.0*alive_count/num_trials],
                     [1.0*dead_utility_at_acquire/num_trials, 1.0*alive_utility_at_acquire/num_trials], 
                     [1.0*dead_time_at_acquire/num_trials, 1.0*alive_time_at_acquire/num_trials]])
    print data
    print time.strftime("%H:%M:%S")
