In [1]:
import csv
import itertools
from deap import base, creator, tools, algorithms
import random
import numpy as np

In [2]:
with open('castle-solutions.csv', newline='', encoding='latin1') as prev_castles_file:
    file_reader = csv.reader(prev_castles_file)
    ARMIES = list(file_reader)

ARMIES = ARMIES[1:]
ARMIES = [tuple(int(n) for n in row[:10]) for row in ARMIES]

TOTAL_SOLDIERS = 100
NUM_CASTLES = 10

POINTS = np.array([1,2,3,4,5,6,7,8,9,10])

In [3]:
def normalize_army(army):
    army_sum = sum(army)

    for i in range(len(army)):
        army[i] = int((army[i]/army_sum)*TOTAL_SOLDIERS)

    army_sum = sum(army)
    
    if army_sum < TOTAL_SOLDIERS:
            army[army.index(min(army))] += TOTAL_SOLDIERS - army_sum
    if army_sum > TOTAL_SOLDIERS:
            army[army.index(max(army))] -= army_sum - TOTAL_SOLDIERS
            
    return army

In [4]:
def generate_random_army():
    army = [random.randint(0,TOTAL_SOLDIERS) for _ in range(NUM_CASTLES)]
    return normalize_army(army)

In [5]:
def test_army_fights(function):
    army = [15, 14, 4, 16, 6, 3, 16, 6, 16, 4]
    
    other_armies = [[(100, 0, 0, 0, 0, 0, 0, 0, 0, 0),      1],
                    [(10, 0, 0, 0, 0, 0, 0, 30, 30, 30),    1],
                    [(5, 5, 0, 0, 0, 0, 0, 30, 30, 30),     1],
                    [(3, 5, 7, 9, 11, 13, 15, 17, 19, 1),   0],
                    [(2, 4, 6, 8, 10, 12, 14, 16, 18, 10),  0],
                    [(2, 2, 4, 5, 7, 10, 10, 15, 20, 25),   0],
                    [(2, 0, 0, 0, 0, 0, 0, 31, 33, 34),     1],
                    [(1, 2, 3, 6, 9, 10, 13, 16, 18, 22),   0],
                    [(1, 1, 1, 15, 15, 15, 16, 16, 10, 10), 0],
                    [(1, 1, 1, 1, 1, 17, 18, 19, 20, 21),   0],
                    [(0, 6, 7, 11, 14, 17, 20, 25, 0, 0),   0],
                    [(0, 0, 12, 16, 11, 0, 14, 0, 47, 0),   1],
                    [(0, 0, 0, 14, 18, 0, 0, 0, 33, 35),    1],
                    [(15, 14, 4, 16, 6, 3, 16, 6, 16, 4),   0],
                    [(14, 13, 6, 16, 6, 3, 16, 6, 16, 4),   0],
                    [(15, 14, 4, 16, 6, 3, 16, 6, 17, 3),   1],
                    [(0, 0, 0, 0, 17, 21, 0, 29, 33, 0),    0]]
    
    for other_army, wins in other_armies:
        f = function(army, [other_army])
        if f != (wins,):
            print(army, other_army, f, wins)

In [6]:
def total_castles_defeated(army, other_armies=()):
    wins = 0
    
    for other_army in other_armies:
        castle_diffs = np.subtract(army, other_army)
    
        other_sum = np.sum(POINTS[np.argwhere(castle_diffs < 0)])
        army_sum = np.sum(POINTS[np.argwhere(castle_diffs > 0)])
        
        if army_sum > other_sum:
            wins += 1
    
    return wins,

In [7]:
test_army_fights(total_castles_defeated)

In [8]:
def mate_armies(army1, army2):
    new_army1, new_army2 = tools.cxTwoPoint(army1, army2)

    new_army1 = normalize_army(new_army1)
    new_army2 = normalize_army(new_army2)

    return new_army1, new_army2

In [9]:
def mutate_army(army, indpb):
    if random.random() < indpb:
        a = random.randint(0, 9)
        b = random.randint(0, 9)
        while a != b and army[b] > 0:
            b = random.randint(0, 9)

        army[a] += 1
        army[b] -= 1

    return army,

In [10]:
def get_best_army(stats):
    fit_values = [army.fitness.values[0] for army in stats]
    index = fit_values.index(max(fit_values))
    return stats[index]

In [11]:
def get_num_unique_armies(stats):
    return len(set(map(tuple, stats)))

In [12]:
def init_individual(army_container):
    return army_container(generate_random_army())

In [13]:
def simulate(population=200,
             generations=20,
             tournament_size=20,
             mating_prob=0.5,
             individual_mutate_prob=0.2,
             hof_size=1):
    toolbox = base.Toolbox()

    creator.create("FitnessMax", base.Fitness, weights=(1.0,))
    creator.create("Army", list, fitness=creator.FitnessMax)

    toolbox.register("individual", init_individual, creator.Army)
    toolbox.register("population", tools.initRepeat, list, toolbox.individual)
    toolbox.register("evaluate", total_castles_defeated, other_armies=ARMIES)
    toolbox.register("mate", mate_armies)
    toolbox.register("mutate", mutate_army, indpb=individual_mutate_prob)
    toolbox.register("select", tools.selTournament, tournsize=tournament_size)

    pop = toolbox.population(n=population)
    hof = tools.HallOfFame(hof_size)

    stats = tools.Statistics(lambda army: army.fitness.values)
    stats.register("avg", np.mean)
    stats.register("min", np.min)
    stats.register("max", np.max)

    best_stats = tools.Statistics(lambda army: army)
    best_stats.register("best", get_best_army)
    best_stats.register("uniq", get_num_unique_armies)

    all_stats = tools.MultiStatistics(scores=stats, boards=best_stats)

    pop, logbook = algorithms.eaSimple(pop,
                                       toolbox,
                                       mating_prob,
                                       individual_mutate_prob,
                                       generations,
                                       stats=all_stats,
                                       halloffame=hof,
                                       verbose=True)

    return pop, logbook, hof

In [14]:
p, l, h = simulate(population=5000, generations=20)


   	      	                   boards                   	       scores      
   	      	--------------------------------------------	-------------------
gen	nevals	best                             	uniq	avg    	max 	min
0  	5000  	[8, 5, 6, 8, 17, 16, 5, 27, 5, 3]	5000	609.603	1138	108
1  	3077  	[5, 8, 6, 10, 11, 24, 25, 2, 5, 4]	3165	813.244	1153	431
2  	2964  	[5, 5, 6, 8, 17, 16, 5, 31, 4, 3] 	2748	959.86 	1196	519
3  	2935  	[5, 6, 6, 8, 17, 16, 3, 31, 5, 3] 	1837	1079.42	1210	749
4  	2938  	[5, 6, 6, 8, 17, 16, 3, 31, 5, 3] 	1084	1132.31	1210	802
5  	3071  	[3, 5, 7, 14, 15, 15, 4, 31, 3, 3]	753 	1175.66	1220	882
6  	2972  	[3, 5, 6, 8, 12, 21, 3, 31, 6, 5] 	295 	1193.14	1231	1009
7  	2991  	[3, 5, 7, 14, 15, 16, 3, 31, 3, 3]	357 	1195.47	1232	999 
8  	3026  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	616 	1194.49	1234	994 
9  	2981  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	281 	1210.32	1234	981 
10 	3060  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	59  	1228   	1234	1065
11 	3032  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	45  	1221.42	1234	1070
12 	2978  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	1   	1234   	1234	1234
13 	3004  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	1   	1234   	1234	1234
14 	2948  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	1   	1234   	1234	1234
15 	2947  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	1   	1234   	1234	1234
16 	2979  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	1   	1234   	1234	1234
17 	2914  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	1   	1234   	1234	1234
18 	3022  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	1   	1234   	1234	1234
19 	3004  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	1   	1234   	1234	1234
20 	2998  	[3, 6, 6, 8, 12, 21, 3, 31, 5, 5] 	1   	1234   	1234	1234