129 lines
4.0 KiB
Elixir
129 lines
4.0 KiB
Elixir
defmodule Diamondtail.Population do
|
|
@moduledoc """
|
|
This module defines an Agent which maintains a list of every individual in the population, allowing an individual to be taken out or introduced.
|
|
"""
|
|
use Agent
|
|
|
|
defmodule Genome do
|
|
@moduledoc """
|
|
Genomes represents the parameters of an individual, which tweaks the weights in the algorithm and implements the genetic algorithm "learning" functionality
|
|
"""
|
|
use TypedStruct
|
|
|
|
typedstruct enforce: true do
|
|
field :mutation_chance, float()
|
|
field :mutation_max_amplitude, float()
|
|
|
|
field :enemy_alive_weight, float()
|
|
field :own_health_weight, float()
|
|
field :own_length_weight, float()
|
|
field :enemy_health_weight, float()
|
|
field :enemy_length_weight, float()
|
|
field :enemy_head_distance_weight, float()
|
|
field :food_distance_weight, float()
|
|
|
|
field :search_depth, float()
|
|
field :value_discount, float()
|
|
end
|
|
|
|
defp random_between(bot, top) do
|
|
bot + :rand.uniform() * (top - bot)
|
|
end
|
|
|
|
@spec mutate(Genome.t()) :: Genome.t()
|
|
def mutate(genome) do
|
|
Map.from_struct(genome)
|
|
|> Enum.map(fn {key, value} ->
|
|
if :rand.uniform() <= genome.mutation_chance do
|
|
{key, value + random_between(-genome.mutation_max_amplitude, genome.mutation_max_amplitude)}
|
|
else
|
|
{key, value}
|
|
end
|
|
end)
|
|
|> Map.new() |> Map.put(:__struct__, Genome)
|
|
end
|
|
|
|
def average(genomes) do
|
|
Enum.reduce(genomes, zero(), fn g, acc ->
|
|
Map.from_struct(acc)
|
|
|> Enum.map(fn {k, v} ->
|
|
{k, v + get_in(g, [Access.key!(k)])}
|
|
end)
|
|
|> Map.new() |> Map.put(:__struct__, Genome)
|
|
end) |> Map.from_struct()
|
|
|> Enum.map(fn {k, v} ->
|
|
{k, v / length(genomes)}
|
|
end) |> Map.new() |> Map.put(:__struct__, Genome)
|
|
end
|
|
|
|
def random() do
|
|
%Genome{
|
|
mutation_chance: random_between(0.1, 0.2),
|
|
mutation_max_amplitude: random_between(0.2, 0.05),
|
|
enemy_alive_weight: random_between(-100.0, -200.0),
|
|
own_health_weight: random_between(0.7, 1.0),
|
|
own_length_weight: random_between(5, 10),
|
|
enemy_health_weight: random_between(-0.8, -0.4), # Summed
|
|
enemy_length_weight: random_between(-10, -5), # Averaged
|
|
|
|
# both of the following are compared to the nearest object, to avoid overrewarding when there are more enemies/foods
|
|
enemy_head_distance_weight: random_between(1, 2), # maximize distance, reward staying away
|
|
food_distance_weight: random_between(-2, -1), # minimize distance, reward staying near
|
|
|
|
search_depth: random_between(3.0, 7.0),
|
|
value_discount: random_between(1.0, 10.0) # as percentage
|
|
}
|
|
end
|
|
|
|
# Not very computationally efficient but avoids repeating code
|
|
def zero() do
|
|
Map.from_struct(random())
|
|
|> Enum.map(fn {key, _value} ->
|
|
{key, 0}
|
|
end)
|
|
|> Map.new() |> Map.put(:__struct__, Genome)
|
|
end
|
|
end
|
|
|
|
def start_link(initial_value) do
|
|
Agent.start_link(fn -> initial_value end, name: __MODULE__)
|
|
end
|
|
|
|
@doc """
|
|
Takes out and returns a random genome from the population. If the population is empty, a new random genome will be introduced.
|
|
"""
|
|
@spec take_random() :: Genome.t()
|
|
def take_random() do
|
|
Agent.get_and_update(__MODULE__, fn list ->
|
|
index = :rand.uniform(length(list)) - 1
|
|
{got, list} = List.pop_at(list, index)
|
|
list = if list == [], do: [Genome.random], else: list
|
|
{got, list}
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Adds a genome to the population
|
|
"""
|
|
@spec add(Genome.t()) :: :ok
|
|
def add(genome) do
|
|
Agent.update(__MODULE__, &[genome | &1])
|
|
end
|
|
|
|
@doc """
|
|
Returns the amount of genomes in the population
|
|
"""
|
|
@spec size() :: integer()
|
|
def size() do
|
|
Agent.get(__MODULE__, &length/1)
|
|
end
|
|
|
|
@doc """
|
|
Returns the "average" genome, to get a sense of the population's tendencies
|
|
"""
|
|
@spec avg() :: Genome.t()
|
|
def avg() do
|
|
Agent.get(__MODULE__, &Genome.average/1)
|
|
end
|
|
end
|