import random
import numpy

## Configuration MasterMind

nb_colors = 5
nb_cases = 5


## Black-box
class MasterMind:
    def __init__(self, nb_colors, nb_cases, solution=None):
        self.nb_colors = nb_colors
        self.nb_cases = nb_cases
        self._solution = solution if solution else self.random_candidate()
    
    def random_candidate(self):
        import random
        return [random.randrange(self.nb_colors) for _ in range(self.nb_cases)]
    
    def try_candidate(self, candidate):
        total = 0
        for i, x in enumerate(self._solution):
            if candidate[i] == x:
                total += 1
        return total
    
    def get_max_radius(self):
        return self.nb_cases
    
    def modify_with_radius(self, x, r):
        import random
        x = x[:]
        items = list(range(self.nb_cases))
        random.shuffle(items)
        for i in items[:r]:
            x[i] = random.randrange(self.nb_colors)
        return x
    
    def is_optimal(self, x, v_x=None):
        v_x = v_x if v_x else self.try_candidate(x)
        return (v_x == self.nb_cases)


class SearchHeuristic:
    def set_problem(self, problem):
        self.problem = problem
    
    def run(self):
        raise Exception('Not implemented !')

# Random Local Search
class RLS(SearchHeuristic):
    def run(self):
        pb = self.problem
        x = pb.random_candidate()
        v_x = pb.try_candidate(x)
        nb_calls = 1
        
        while not pb.is_optimal(x, v_x):
            r = 1
            y = pb.modify_with_radius(x, r)
            v_y = pb.try_candidate(y)
            if v_y >= v_x:
                x = y
                v_x = v_y
            nb_calls += 1
        
        return (x, v_x, nb_calls)
        
class EA(SearchHeuristic):
    def __init__(self, proba, lda, mu):
        self.proba = proba
        self.lda = lda
        self.mu = mu
    
    def run(self):
        pb = self.problem
        
        x_list = []
        
        for _ in range(self.mu):
            x = pb.random_candidate()
            vx = pb.try_candidate(x)
            x_list.append((x, vx))
        x_list = sorted(x_list, key=lambda x: x[1], reverse=True)
        x_best = x_list[0]
        nb_calls = 1
        
        while not pb.is_optimal(x_best[0], x_best[1]):
            y_list = []
            
            for _ in range(self.lda):
                r = numpy.random.binomial(pb.get_max_radius(), self.proba)
                y = pb.modify_with_radius(x_best[0], r)
                vy = pb.try_candidate(y)
                y_list.append((y, vy))
            #print(x_list, y_list)
                
            selection = x_list + y_list
            selection = sorted(selection, key=lambda x: x[1], reverse=True)
            x_list = selection[:self.mu]
            x_best = x_list[0]
            nb_calls += 1
                    
        return (x_best[0], x_best[1], nb_calls)

def avg_calls(algo, problem, nb=100):
    nb_calls_list = []
    for _ in range(100):
        algo.set_problem(problem)
        _, _, nb_calls = algo.run()
        nb_calls_list.append(nb_calls)
    return numpy.mean(nb_calls_list)

## Main
avg = avg_calls(RLS(), MasterMind(nb_colors, nb_cases))
print('Average RLS: %s' % avg)
avg = avg_calls(EA(1./nb_cases, 1, 1), MasterMind(nb_colors, nb_cases))
print('Average EA: %s' % avg)


print('=================')
probas = list(numpy.arange(0.05, 0.5, 0.01))
p_best_list = []
for n in range(5, 15):
    avg_list = []
    for p in probas:
        avg = avg_calls(EA(p, 1, 1), MasterMind(n, n))
        avg_list.append(avg)
        #print('{}%'.format(int(p*100)))
    p_best = probas[numpy.argmin(avg_list)]
    p_best *= n
    p_best_list.append(p_best)
    print('{}: {}'.format(n, p_best))

from matplotlib import pyplot as plt
x = list(range(5, 15))
y = p_best_list
plt.plot(x, y)
plt.savefig('foo3.png')
