/*
* Neural Tanks is a game created by Eddie O'Hagan that leverages Neural Networking
* to use for AI. This was created in Unreal Engine 4 which is developed by Epic Games.
* (Copyright Epic Games, Inc. All Rights Reserved.)
*
* Smart Tanks was originally developed by Mat Buckland in 2002 as an example
* on how Neural Networking can be used to train a set of minesweepers
* to collect mines. I (Eddie O'Hagan) updated this project in 2024 to use more
* Object Oriented Programming and modern practices. To see the original tutorial;
* visit <see href="http://www.ai-junkie.com/ann/evolved/nnt1.html">Neural Networking Tutorial</see>
* and <see href="http://www.ai-junkie.com/ga/intro/gat1.html">Genetic Algorithm Tutorial</see>
*/
#include "Chromosome.h"

/// <summary>
/// Default Constructor.
/// </summary>
UChromosome::UChromosome()
{
	myFitness = 0.0;
	myCrossoverRate = 0.7;
	myMutationRate = 0.1;
	myMutationMaxPerturbation = 0.3;
}

/// <summary>
/// Sets/Updates this chromosome's data with the provided data. This generates <c>numberOfWeights</c> weights.
/// </summary>
/// <param name="numberOfWeights">The total number of weights this chromosome will hold. (Generates random number of weights)</param>
/// <param name="crossoverRate">A rate to determine if/where to cross over genes of another chromosome.</param>
/// <param name="mutationRate">A rate to determine if/where a single weight will be modified slightly.</param>
void UChromosome::Initialize(const int numberOfWeights, const float crossoverRate, const float mutationRate)
{
	myFitness = 0.0;
	myCrossoverRate = crossoverRate;
	myMutationRate = mutationRate;
	myMutationMaxPerturbation = 0.3;

	for (int count = 1U; count <= numberOfWeights; count++)
	{
		myWeights.Add(FMath::FRandRange(-1.0f, 1.0f));
	}
}

/// <summary>
/// Sets/Updates this chromosome's data with the provided data. This generates <c>numberOfWeights</c> weights.
/// </summary>
/// <param name="weights">The collection of weights to use for this chromosome.</param>
/// <param name="fitness">The fitness score of this chromosome.</param>
/// <param name="crossoverRate">A rate to determine if/where to cross over genes of another chromosome.</param>
/// <param name="mutationRate">A rate to determine if/where a single weight will be modified slightly.</param>
void UChromosome::Initialize(const TArray<float>& weights, const float fitness, const float crossoverRate, const float mutationRate)
{
	myWeights = weights;
	myFitness = fitness;
	myCrossoverRate = crossoverRate;
	myMutationRate = mutationRate;
	myMutationMaxPerturbation = 0.3;
}

/// <summary>
/// Add a weight to this chromosome's weight collection.
/// </summary>
/// <param name="weight">The weight value to add.</param>
void UChromosome::AddWeight(const float weight)
{
	myWeights.Add(weight);
}

/// <summary>
/// Removes all weights from this chromosome's weight collection.
/// </summary>
void UChromosome::ClearWeights()
{
	myWeights.Empty();
}

/// <summary>
/// Copies the data from the provided chromosome to this one.
/// </summary>
/// <param name="chromosomeToCopy">The chromosome to copy data from.</param>
void UChromosome::CopyChromosomeData(UChromosome* chromosomeToCopy)
{
	//int numWeights = myWeights.Num();
	int numWeights = chromosomeToCopy->GetNumberOfWeights();

	myFitness = chromosomeToCopy->GetFitness();
	myCrossoverRate = chromosomeToCopy->GetCrossoverRate();
	myMutationRate = chromosomeToCopy->GetMutationRate();
	myMutationMaxPerturbation = 0.3;

	for (int index = 0; index < numWeights; index++)
	{
		myWeights[index] = chromosomeToCopy->GetWeightAtIndex(index);
	}
}

/// <summary>
/// Attempts to "mate" or crossover the genes of two chromosomes to form two new babies, 
/// This allows for the neural network to naturally grow and attempt to solve the problem 
/// with previous generations of chromosomes but with possible improvements. 
/// </summary>
/// <param name="thisPartner">This chromosome.</param>
/// <param name="otherPartner">The other chromosome you want to cross weights with.</param>
/// <param name="outBabyOne">One baby is produced with one swap.</param>
/// <param name="outBabyTwo">The second baby is produced with the opposite swap.</param>
/// <returns>true if new babies were produced, false to just re-use the original parents.</returns>
bool UChromosome::Crossover(UChromosome* thisPartner, UChromosome* otherPartner, UChromosome* outBabyOne, UChromosome* outBabyTwo)
{
	int index;
	int crossOverIndex;
	int numberOfWeights;

	if (FMath::FRandRange(0.0f, 1.0f) <= myCrossoverRate && this->GetIsEqual(otherPartner) == false)
	{
		//Remove any previous weights.
		outBabyOne->ClearWeights();
		outBabyTwo->ClearWeights();

		//Generate a random index to start the crossover from.
		numberOfWeights = myWeights.Num();
		crossOverIndex = FMath::RandRange(0, numberOfWeights - 1);

		//Copy over this parents to baby one (and the other to baby two) up to the crossOverIndex.
		for (index = 0U; index < crossOverIndex; index++)
		{
			outBabyOne->AddWeight(myWeights[index]);
			outBabyTwo->AddWeight(otherPartner->GetWeightAtIndex(index));
		}

		//Starting at the cross over index, swap the partners until the end.
		for (index = crossOverIndex; index < numberOfWeights; index++)
		{
			outBabyOne->AddWeight(otherPartner->GetWeightAtIndex(index));
			outBabyTwo->AddWeight(myWeights[index]);
		}

		return true;
	}

	return false;
}

/// <summary>
/// Attempts to perturb or slightly change a single weight based on the mutation rate.
/// </summary>
void UChromosome::Mutate()
{
	//Mutation is performed for each weight in the chromosome.
	for (int index = 0U; index < myWeights.Num(); index++)
	{
		if (FMath::FRandRange(0.0f, 1.0f) < myMutationRate)
		{
			//Add or subtract a small value to the weight.
			myWeights[index] += (FMath::FRandRange(-1.0f, 1.0f) * myMutationMaxPerturbation);
		}
	}
}

/// <summary>
/// Allows us to use the std::sort function to sort by fitness score.
/// </summary>
/// <param name="otherChromosome">The other chromosome you are comparing.</param>
/// <returns>true if this chromosome has a lower score then the other, false otherwise.</returns>
bool UChromosome::operator<(const UChromosome& otherChromosome)
{
	return myFitness < otherChromosome.GetFitness();
}

/// <summary>
/// Compares two chromosomes to see if they are equal based on weight values.
/// </summary>
/// <param name="otherChromosome">The other chromosome you are comparing.</param>
/// <returns>true if all the weights of the two chromosomes are equal, false otherwise.</returns>
bool UChromosome::GetIsEqual(const UChromosome* otherChromosome) const
{
	for (int weightIndex = 0U; weightIndex < GetNumberOfWeights(); weightIndex++)
	{
		if (myWeights[weightIndex] != otherChromosome->GetWeightAtIndex(weightIndex))
		{
			return false;
		}
	}

	return true;
}

/// <summary>
/// Returns the fitness score of this chromosome.
/// </summary>
/// <returns>The fitness score of this chromosome.</returns>
float UChromosome::GetFitness() const
{
	return myFitness;
}

/// <summary>
/// Returns the crossover rate of this chromosome.
/// </summary>
/// <returns>The crossover rate of this chromosome.</returns>
float UChromosome::GetCrossoverRate() const
{
	return myCrossoverRate;
}

/// <summary>
/// Returns the mutation rate of this chromosome.
/// </summary>
/// <returns>The mutation rate of this chromosome.</returns>
float UChromosome::GetMutationRate() const
{
	return myMutationRate;
}

/// <summary>
/// Returns the number of total weights in this chromosome.
/// </summary>
/// <returns>The total number of weights in this chromosome.</returns>
int UChromosome::GetNumberOfWeights() const
{
	return myWeights.Num();
}

/// <summary>
/// Returns a single weight at a specified index.
/// </summary>
/// <param name="index">The specific index of the weight.</param>
/// <returns>The weight value at the provided index, -1.0 and throws exception if not found.</returns>
float UChromosome::GetWeightAtIndex(const int index) const
{
	if (index < GetNumberOfWeights())
	{
		return myWeights[index];
	}
	else
	{
		throw new std::exception("UChromosome::GetWeightAtIndex(const int) failed, index is out of bounds.");
	}

	return -1.0;
}

/// <summary>
/// Returns the weight collection.
/// </summary>
/// <returns>A reference to the weight collection.</returns>
TArray<float>& UChromosome::GetWeights()
{
	return myWeights;
}

/// <summary>
/// Updates the fitness score with the provided value.
/// </summary>
/// <param name="fitness">The updated fitness score.</param>
void UChromosome::SetFitness(const float& fitness)
{
	myFitness = fitness;
}

/// <summary>
/// Updates the crossover rate with the provided value.
/// </summary>
/// <param name="crossoverRate">The updated crossover rate.</param>
void UChromosome::SetCrossoverRate(const float& crossoverRate)
{
	myCrossoverRate = crossoverRate;
}

/// <summary>
/// Updates the crossover rate with the provided value.
/// </summary>
/// <param name="mutationRate">The updated crossover rate.</param>
void UChromosome::SetMutationRate(const float mutationRate)
{
	myMutationRate = mutationRate;
}

/// <summary>
/// Clears the current collection of weights and updates the collection with 
/// the provided weights vector.
/// </summary>
/// <param name="weights">The new weights to use.</param>
void UChromosome::SetWeights(TArray<float> weights)
{
	myWeights.Empty();
	for (int weightIndex = 0U; weightIndex < weights.Num(); weightIndex++)
	{
		myWeights.Add(weights[weightIndex]);
	}
}