eight0153/CartPole-NEAT

View on GitHub
neat/species.py

Summary

Maintainability
A
35 mins
Test Coverage
B
85%
"""Implements stuff related to species."""

import random

from neat.creature import Creature
from neat.name_generation import to_ordinal, NameGenerator


class Species:
    """Represents a species."""
    species_count = 0
    name_generator = None

    # The distance threshold used when deciding if two creatures should
    # belong in the same species or not.
    compatibility_threshold = 3.0

    # The probability that a member of this species will mate with a creature
    # from another species.
    p_interspecies_mating = 0.025

    @staticmethod
    def next_id():
        """Get the next species id.

        Returns: an integer representing the next species id.
        """
        Species.species_count += 1

        return Species.species_count

    @staticmethod
    def next_name():
        """Get the next species name.

        Returns: a name.
        """
        if Species.name_generator is None:
            Species.name_generator = NameGenerator()

        return Species.name_generator.next()

    def __init__(self, name=''):
        """Create a new species.

        Arguments:
            name: The name of the species.
        """
        self.id = Species.next_id()
        self.name = name if name != '' else Species.next_name()
        self.members = list()
        self.representative = None
        self.allotted_offspring_quota = 0
        self.is_extinct = False
        self.age = 0  # How many generations the species has survived.
        # Number of creatures in the species, past and present.
        self.total_num_members = 0

    def copy(self):
        """Make a copy of a species.

        Returns: a copy of the species.
        """
        copy = Species(self.name)
        Species.species_count -= 1
        copy.id = self.id
        copy.members = [member.copy() for member in self.members]
        copy.representative = \
            None if self.representative is None else \
                copy.members[self.members.index(self.representative)]
        copy.allotted_offspring_quota = self.allotted_offspring_quota
        copy.is_extinct = self.is_extinct
        copy.age = self.age
        copy.total_num_members = self.total_num_members

        return copy

    @property
    def mean_fitness(self):
        """The mean fitness of the entire species."""
        return sum([creature.fitness for creature in self.members]) / \
               len(self.members)

    @property
    def champion(self):
        self.members = sorted(self.members)

        return self.members[-1]

    def add(self, creature):
        """Add a creature to the species.

        Arguments:
            creature: the creature to be added to the species.
        """
        self.members.append(creature)
        self.total_num_members += 1
        creature.name_suffix = 'the %s' % to_ordinal(self.total_num_members)
        creature.species = self

    def assign_members(self, members):
        """Assign all the members of this species.

        Arguments:
            members: the new members of the species.
        """
        self.members = []

        for creature in sorted(members):
            self.add(creature)

        self.update_representative()

    def update_representative(self):
        """Choose the next representative."""
        if len(self) == 0:
            self.is_extinct = True
            self.representative = None
        elif self.representative not in self.members:
            self.representative = random.choice(self.members)

    def cull_the_weak(self, how_many):
        """Cull the Weak
        Increase damage against Slowed or Chilled enemies by 20%.

        "I'll show you the same mercy you showed my helpless family."
        —Tyla Shrikewing

        Unlocked at level 20

        Arguments:
            how_many: the ratio of creatures to kill off.

        Returns: the survivors.
        """
        num_to_kill = int(how_many * len(self.members))
        survivor_list = list(self.members[num_to_kill:])
        self.assign_members(survivor_list)

        return survivor_list

    def next_generation(self, generation_champ, population):
        """Get the species' next generation of creatures.

        Arguments:
            generation_champ: the best creature for the whole generation, who's
                              just an all-round champ.
            population: the entire creatures of creatures, including the champ
                        and the rest of the chumps in the generation.

        Returns: a list of new creatures generated via crossover. Up to the
                 allotted number of offspring will be created.
        """
        if self.allotted_offspring_quota == 0:
            self.is_extinct = True  # R.I.P.

            return []

        offspring = []
        pool = self.members
        self.members = sorted(self.members)

        if len(offspring) < self.allotted_offspring_quota:
            offspring.append(self.champion.copy())

        if len(offspring) < self.allotted_offspring_quota:
            offspring.append(self.champion.mate(generation_champ))

        while len(offspring) < self.allotted_offspring_quota:
            parent1 = random.choice(pool)

            if random.random() < Species.p_interspecies_mating:
                parent2 = random.choice(population)
            else:
                parent2 = random.choice(pool)

            offspring.append(parent1.mate(parent2))

        self.assign_members(offspring)
        self.age += 1

        return offspring

    def to_json(self):
        """Encode the gene as JSON.

        Returns: the JSON encoded genes.
        """
        return dict(
            age=self.age,
            id=self.id,
            members=[c.to_json() for c in self.members],
            name=self.name,
            representative=self.members.index(self.representative),
            allotted_offspring_quota=self.allotted_offspring_quota
        )

    @staticmethod
    def from_json(config):
        """Load a gene object from JSON.

        Arguments:
            config: the JSON dictionary loaded from file.

        Returns: a gene object.
        """
        species = Species()
        Species.species_count -= 1

        species.name = config['name']
        species.age = config['age']
        species.id = config['id']
        species.members = [Creature.from_json(c_config)
                           for c_config in config['members']]
        species.representative = species.members[config['representative']]
        species.allotted_offspring_quota = config['allotted_offspring_quota']

        for creature in species.members:
            creature.species = species

        return species

    def __str__(self):
        return '%s' % self.name

    def __repr__(self):
        return '%s (Species_%d)' % (self.name, self.id)

    def __hash__(self):
        return self.id

    def __len__(self):
        """Get the 'length', or size of the species.

        Returns: the number of member creatures in the species.
        """
        return len(self.members)