# Python implementation of Richard Dawkin's weasel program
# Illustrates power of cumulative selection vs entirely random search
# This version allows all positions to mutate in any generation, meaning correct letters can be reversed

# Initializations
import sys, random
target = list("METHINKS IT IS LIKE A WEASEL")   # Target sequence
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ "        # Alphabet of possible symbols (uppercase letters + blank)
n_offspring = 50                                # Number of offspring per generation
mut_rate = 0.08                                 # Mutation rate: probability that any given letter will change
best_offspring = []                             # Variable containing best offspring after each generation
                                                # Contains empty list when loop is first entered

# Construct random starting string of same length as target
parent = []
for i in range(len(target)):
    parent.append(random.choice(alphabet))

# Main loop: construct mutated offspring, select best for next generation, stop when target reached
gen = 0
while best_offspring != target:
    gen = gen + 1

    # Keep track of the number of good, bad, and neutral mutations arising in this generation
    n_beneficial = n_detrimental = n_neutral = num_mut = 0.0

    # Keep track of how many kids in current generation that look exactly like their parent
    num_unchanged = n_offspring

    # Construct n_offspring children that may contain mutations
    kid_list = []
    for i in range(n_offspring):

        # Copy content of parent to kid
        kid = parent[:]

        # Iterate over positions in kid, for each position allow possibility of mutating
        kid_changed = False
        for pos in range(len(kid)):

            # Let residue mutate with probability mut_rate
            if random.random() < mut_rate:
                kid_changed = True
                num_mut += 1

                # Randomly select new symbol (that is different from the one already present)
                old_symbol = parent[pos]
                possible_new_symbols = set(alphabet) - set(old_symbol)
                new_symbol = random.choice(list(possible_new_symbols))

                # Mutate kid
                kid[pos] = new_symbol

                # Check if mutation was beneficial, detrimental, or neutral and update count
                if old_symbol == target[pos]:           # Correct letter has been changed: detrimental
                    n_detrimental += 1
                elif new_symbol == target[pos]:         # Incorrect letter has been changed to correct: beneficial
                    n_beneficial += 1
                else:                                   # Incorrect letter has been changed to other incorrect letter: neutral
                    n_neutral += 1

        # If kid was mutated, then it no longer looks like its parent
        if kid_changed:
            num_unchanged -= 1

        # Add (possibly) mutated kid to list of current kids
        kid_list.append(kid)


    # Find most fit offspring (= string most similar to target)
    smallest_dif = len(target) + 1
    for kid in kid_list:

        # Find number of positions that differ between kid and target
        dif = 0.0
        for pos in range(len(target)):
            if kid[pos] != target[pos]:
                dif = dif + 1

        # Keep best kid found so far
        if dif < smallest_dif:
            smallest_dif = dif
            best_offspring = kid

    # Use best offspring as starting point for next round of mutation
    parent = best_offspring

    # Print progress
    fitness = (len(target)-smallest_dif)/len(target)            # Fitness of most fit individual
    ben_frac = n_beneficial/num_mut
    detr_frac = n_detrimental/num_mut
    neu_frac = n_neutral/num_mut
    result_string = ""
    for pos in range(len(target)):
        if best_offspring[pos] == target[pos]:
            result_string += best_offspring[pos]
        else:
            result_string += best_offspring[pos].lower()
    print "%s     ** Gen: %4d   Dif: %3d   Fit: %.4f   Bene: %.4f  Detr: %.4f  Neu: %.4f   Unchanged: %3d" % (result_string,
                                                                                                gen, smallest_dif, fitness,
                                                                                                ben_frac, detr_frac, neu_frac,
                                                                                                num_unchanged)

