AI Assignment
We were tasked to add AI to pre-made environments
- > Artificial Life
- > Decision Theory
- > Neural Network
- > Evolutionary Algorithms
Learn More
Project Overview
For my Level 6 Ai Assignment we we're given 4 different applications and tasked to implement different AIs into each of the applications.
- > Artificial Life - A simulation of how fish flock with each other and how a shark would disperse a flock.
- > Decision Theory - Create a chess AI which would make intelligent moves and not blunder pieces - MinMax Algorithms
- > Neural Network - Create a Neural Network to play "Flappy Bird"
- > Evolutionary Algorithms - Create a genetic algorithm to play a tower defense game.
Artificial Life
Artificial Life was a project where we were tasked to simulate a fish school and how they would act and respond to predators.
To do this I used steering methods to simulate the different behaviors of the fish and shark
Fish would utilise flocking and flee steering methods as in real life fish seek safety in numbers and they will flee when a shark
is detected as they value their lives over staying as a school.
Sharks on the other hand will actively seek and pursue these fish as they need to eat them to survive.
We can see in the screenshot on the left that the fish are flocking together using Craig Reynolds rules of Alignment, Cohesion, and Separation.
void Boid::Flock(vecBoid nearBoids)
{
// Only run if there are fish to flock with
if (nearBoids.size() > 0)
{
// alignment - allign the fish with the average direction of other fish
XMFLOAT3 alignment = calculateAlignmentVector(&nearBoids);
// Cohesion - move the fish to the average position of the fish
XMFLOAT3 cohesion = calculateCohesionVector(&nearBoids);
// Separation - separate the fish from one another
XMFLOAT3 separation = calculateSeparationVector(&nearBoids);
// Apply the weightings
alignment = multiplyFloat3(alignment, ALIGNMENT_WEIGHT);
cohesion = multiplyFloat3(cohesion, COHESION_WEIGHT);
separation = multiplyFloat3(separation, SEPARATION_WEIGHT);
// Apply the forces
m_direction = addFloat3(m_direction, alignment);
m_direction = addFloat3(m_direction, cohesion);
m_direction = addFloat3(m_direction, separation);
}
}
void Boid::Seek(vecBoid nearBoids)
{
// Only Seek if there are boids nearby
if (nearBoids.size() > 0)
{
// Return Value
XMFLOAT3 desiredLocation = XMFLOAT3(0, 0, 0);
// Set values for the nearest fish to reduce errors
Boid* currentClosestFish = nearBoids[0];
float currentClosestFishDistance = SHARK_VISION_DISTANCE;
for (Boid* boid : nearBoids)
{
XMFLOAT3 otherPos = *boid->getPosition();
// Work out the distance between the boid
float distance = sqrt(((m_position.x - otherPos.x) * (m_position.x - otherPos.x))
+ ((m_position.y - otherPos.y) * (m_position.y - otherPos.y)));
// Compare it with the current closest boid
if (distance < currentClosestFishDistance)
{
// If its closer make it the new closes boid
currentClosestFish = boid;
currentClosestFishDistance = distance;
}
}
// Set the desired location to the closest boids postion
desiredLocation = subtractFloat3(*currentClosestFish->getPosition(), m_position);
// set the speed
desiredLocation = setVectorMagnitude(desiredLocation, SHARK_SPEED);
// Steering
desiredLocation = subtractFloat3(desiredLocation, this->m_direction);
// Apply the force
m_direction = addFloat3(m_direction, desiredLocation);
}
}
Decision Theory
For Decision Theory we were tasked to create a chess ai which would make the best possible moves and prevent blundering.
For example the AI must recognise that taking pieces may not be the best move if it costs them more valuable pieces.
The MiniMax algorithm has been utilised however throughout the project I researched into other algorithms and attempted to implement them
For example the NegaMax algorithm is more suitable in a chess ai as it cuts the codebase down a considerable amount.
To also increase the performance of the algorithm and reduce the overall speed it takes to come to a decision
int ChessPlayer::AlphaBeta(Board* board, int depth, bool maximisingPlayer, int alpha, int beta)
{
if (depth == 0)
{
//cout << "+++++++++++++++++++++" << endl;
//PrintBoard(board);
return EvaluateBoard(board, m_colour);
}
if (maximisingPlayer)
{
int maxEval = -100000000;
vecPieces vPieces;
unsigned int piecesAvailable = getAllLivePiecesOnTeamOnBoard(vPieces, m_colour, board);
// For every valid move
for (int currentPiece = 0; currentPiece < vPieces.size(); currentPiece++)
{
alpha = -100000000;
beta = 100000000;
vector> avaliableMoves = getValidMovesForPiece(board, vPieces[currentPiece]);
for (int currentMove = 0; currentMove < avaliableMoves.size(); currentMove++)
{
// Apply move
int rowStart = avaliableMoves[currentMove].get()->getOriginPosition().first;
int colStart = avaliableMoves[currentMove].get()->getOriginPosition().second;
int rowFin = avaliableMoves[currentMove].get()->getDestinationPosition().first;
int colFin = avaliableMoves[currentMove].get()->getDestinationPosition().second;
std::shared_ptr removedPiece;
if (board->getSquare(rowFin, colFin)->occupiedState())
{
removedPiece = board->getSquare(rowFin, colFin)->getOccupyingPiece();
board->getSquare(rowFin, colFin)->removeOccupyingPiece();
}
board->getSquare(rowStart, colStart)->removeOccupyingPiece();
board->getSquare(rowFin, colFin)->occupySquare(vPieces[currentPiece].piece);
int currentEval = AlphaBeta(board, depth - 1, false, alpha, beta);
// Revert move
board->getSquare(rowStart, colStart)->occupySquare(vPieces[currentPiece].piece);
board->getSquare(rowFin, colFin)->removeOccupyingPiece();
if (removedPiece != nullptr)
{
board->getSquare(rowFin, colFin)->occupySquare(removedPiece);
}
//cout << "Piece: " << currentPiece << " Move: " << currentMove << " Score: " << currentEval << endl;
if (currentEval > maxEval)
{
//cout << "Piece: " << currentPiece << " Move: " << currentMove << endl;
maxEval = currentEval;
m_bestMove.move = avaliableMoves[currentMove];
m_bestMoves.clear();
m_bestMoves.push_back(avaliableMoves[currentMove]);
}
if (currentEval == maxEval)
{
m_bestMoves.push_back(avaliableMoves[currentMove]);
}
alpha = max(alpha, maxEval);
if (maxEval > beta)
{
//cout << "Pruned" << endl;
break;
}
}
}
return maxEval;
}
else
{
int minEval = 100000000;
vecPieces vPieces;
unsigned int piecesAvailable = getAllLivePiecesOnTeamOnBoard(vPieces, m_opposingColour, board);
// For every valid move
for (int currentPiece = 0; currentPiece < vPieces.size(); currentPiece++)
{
//cout << "Next piece" << endl;
vector> avaliableMoves = getValidMovesForPiece(board, vPieces[currentPiece]);
for (int currentMove = 0; currentMove < avaliableMoves.size(); currentMove++)
{
// Apply move
int rowStart = avaliableMoves[currentMove].get()->getOriginPosition().first;
int colStart = avaliableMoves[currentMove].get()->getOriginPosition().second;
int rowFin = avaliableMoves[currentMove].get()->getDestinationPosition().first;
int colFin = avaliableMoves[currentMove].get()->getDestinationPosition().second;
std::shared_ptr removedPiece;
if (board->getSquare(rowFin, colFin)->occupiedState())
{
removedPiece = board->getSquare(rowFin, colFin)->getOccupyingPiece();
board->getSquare(rowFin, colFin)->removeOccupyingPiece();
}
board->getSquare(rowStart, colStart)->removeOccupyingPiece();
board->getSquare(rowFin, colFin)->occupySquare(vPieces[currentPiece].piece);
int currentEval = AlphaBeta(board, depth - 1, false, alpha, beta);
// Revert move
board->getSquare(rowStart, colStart)->occupySquare(vPieces[currentPiece].piece);
board->getSquare(rowFin, colFin)->removeOccupyingPiece();
if (removedPiece != nullptr)
{
board->getSquare(rowFin, colFin)->occupySquare(removedPiece);
}
if (currentEval < minEval)
{
//cout << "Piece: " << currentPiece << " Move: " << currentMove << endl;
minEval = currentEval;
//m_bestMove.move = avaliableMoves[currentMove];
}
beta = min(beta, minEval);
if (minEval < alpha)
{
//cout << "Pruned" << endl;
break;
}
}
}
return minEval;
}
}
Neural Network
For the neural network project our task was to implement a neural network into flappy bird to allow the birds to complete the game.
However a neural network alone will not allow for any eveolution of the birds and therefore a genetic algorithm has also been used in this application
to allow for the birds to evolve and become better than the last birds.
Since the Genetic Algorithm code is similar to the Evolutionary Algorithm Project I will only include Neural Network Code snippets in this section
I also made a lot of changes to the base game as it only allowed 1 bird and had logic for 1 bird, adding the ability to have more birds on the screen allows
us to see the best birds per generation within real-time and made the application run quicker as whole generations are being played at a time.
The input nodes took in the distance to the center of the gap between the pipes, the distance to the floor, and the distance to the next set of pipes,
the neural network then processes this data and returns one output which is wether the bird should flap or not flap.
bool AIController::NeuralNetworkLogic(Land* land, Bird* bird, Pipe* pipe)
{
// Input nodes
_neuralNetwork[0][0] = distanceToCentreOfPipeGap(pipe, bird);
_neuralNetwork[0][1] = distanceToFloor(land, bird);
_neuralNetwork[0][2] = distanceToNearestPipes(pipe, bird);
for (int i = 0; i < _neuralNetwork.size() - 1; i++)
{
for (int j = 0; j < _neuralNetwork[i + 1].size(); j++)
{
for (int k = 0; k < _neuralNetwork[i].size(); k++)
{
_neuralNetwork[i + 1][j] += _neuralNetwork[i][k] * _NNweights[i][k][j];
}
// activation - hyperbolic tangent
if (0 >= _neuralNetwork[i + 1][j])
{
_neuralNetwork[i + 1][j] = pow(2, _neuralNetwork[i + 1][j]) - 1;
}
else
{
_neuralNetwork[i + 1][j] = 1 - pow(2, -_neuralNetwork[i + 1][j]);
}
}
}
return 0 <= _neuralNetwork[2][0];
}
Evolutionary Algorithms
For the Evolutionary Algorithms project our task was to implement a genetic algorithm into the application to achieve a score of 190+ in the game.
The Genetic Algorithms initial population is created through random values and then run, each different solution will have its own fitness and this is what is used
to determine which solutions are chosen within the generation of the next population.
To add some variation to the algorithm to prevent the algorithm converging too quickly I implemented mutation. The mutation is a small chance and it mutates some
of the genes of the chromosome. To show how the genetic algorithm is layed out within my code I created a diagram. One gene is a tower and the information a tower holds,
and a chromosome is a list of these genes to create a final solution
void Population::GenerateNextPopulation()
{
// Firstly lets order the chromosomes based on their performance and give them a rank
SortChromosomes();
m_lastGenerationBest = m_Chromosomes.back();
// Now we should give these chromosomes their ranks
// We are giving them ranks as Ranked selection gives stronger chromosomes than roulettwheel
for (int i = 0; i < m_Chromosomes.size(); i++)
{
m_Chromosomes[i]->SetRank(i + 1);
}
vector nextPopulation;
// Elitism - allows for a stronger next generation
Chromosome* bestChromosome = m_Chromosomes.back();
bestChromosome->SetCurrentGene(0);
bestChromosome->Print();
nextPopulation.push_back(bestChromosome);
// Now we've given them ranks lets do reproduction to create the next population
for (int i = 0; i < m_Chromosomes.size()-1; i += 2)
{
Chromosome* parent1 = RouletteWheelSelection();
Chromosome* parent2 = RouletteWheelSelection();
while (parent1 == parent2)
{
parent2 = RouletteWheelSelection();
}
Chromosome* offspring1 = Reproduce(parent1, parent2);
Chromosome* offspring2 = Reproduce(parent2, parent1);
// now that we have some children lets add some mutation
int mutationRoll1 = AIController::GenerateRandomNum(0, 100);
if (mutationRoll1 <= m_iMutationChance)
{
offspring1 = Mutate(offspring1);
}
nextPopulation.push_back(offspring1);
int mutationRoll2 = AIController::GenerateRandomNum(0, 100);
if (mutationRoll2 <= m_iMutationChance)
{
offspring2 = Mutate(offspring2);
}
if (1 != m_Chromosomes.size() -1)
{
nextPopulation.push_back(offspring2);
}
}
//Swap the populations over
m_Chromosomes.clear();
cout << nextPopulation.size() << endl;
for (int i = 0; i < nextPopulation.size(); i++)
{
m_Chromosomes.push_back(nextPopulation[i]);
}
m_iGenerationCount++;
}
Chromosome* Population::Reproduce(Chromosome* parent1, Chromosome* parent2)
{
// time to create a offspring from 2 parents
Chromosome* offspring = new Chromosome();
for (int i = 0; i < parent1->GetGeneCount(); i += 2)
{
offspring->AddGene(parent1->GetGeneAt(i));
// Only add the gene if we have space
if (i != parent1->GetGeneCount() - 1)
{
offspring->AddGene(parent2->GetGeneAt(i+1));
}
}
return offspring;
}
Chromosome* Population::Mutate(Chromosome* c)
{
Chromosome* returnChromosome = new Chromosome();
for (int i = 0; i < c->GetGeneCount(); i++)
{
returnChromosome->AddGene(c->GetGeneAt(i));
}
int mutationTypeRoll = AIController::GenerateRandomNum(1, 5);
if (mutationTypeRoll == 1)
{
cout << "Swap Mutation" << endl;
// Swap mutation
int randomIndex1 = AIController::GenerateRandomNum(0, c->GetGeneCount() - 1);
int randomIndex2 = AIController::GenerateRandomNum(0, c->GetGeneCount() - 1);
// make sure indexs are different
while (randomIndex1 == randomIndex2)
{
randomIndex2 = AIController::GenerateRandomNum(0, c->GetGeneCount() - 1);
}
cout << randomIndex1 << " " << randomIndex2 << endl;
Gene temp;
temp = returnChromosome->GetGeneAt(randomIndex1);
returnChromosome->SetGeneAt(randomIndex1, returnChromosome->GetGeneAt(randomIndex2));
returnChromosome->SetGeneAt(randomIndex2, temp);
return returnChromosome;
}
else if (mutationTypeRoll == 2)
{
cout << "Reverse Mutation" << endl;
// Reverse Mutation
int randomIndex1 = AIController::GenerateRandomNum(0, c->GetGeneCount() - 1);
int randomIndex2 = AIController::GenerateRandomNum(0, c->GetGeneCount() - 1);
// make sure indexs are different
while (randomIndex1 == randomIndex2)
{
randomIndex2 = AIController::GenerateRandomNum(0, c->GetGeneCount() - 1);
}
// make sure random number 1 is smaller
if (randomIndex2 < randomIndex1)
{
int temp = randomIndex1;
randomIndex1 = randomIndex2;
randomIndex2 = temp;
}
// Get section to flip
vector selectedGenes;
for (int i = randomIndex1; i < randomIndex2; i++)
{
selectedGenes.push_back(returnChromosome->GetGeneAt(i));
}
// Perform flip
int n = selectedGenes.size() - 1;
for (int i = randomIndex1; i < randomIndex2; i++)
{
returnChromosome->SetGeneAt(i, selectedGenes[n]);
n--;
}
return returnChromosome;
}
else
{
cout << "Reverse Random" << endl;
// Random Mutation
int randomIndex1 = AIController::GenerateRandomNum(0, c->GetGeneCount() - 1);
int randomIndex2 = AIController::GenerateRandomNum(0, c->GetGeneCount() - 1);
cout << randomIndex1 << endl;
cout << randomIndex2 << endl;
// make sure indexs are different
while (randomIndex1 == randomIndex2)
{
randomIndex2 = AIController::GenerateRandomNum(0, c->GetGeneCount() - 1);
}
Gene tp;
tp.x = AIController::GenerateRandomNum(0, 25);
tp.y = AIController::GenerateRandomNum(0, 17);
tp.towerType = (TowerType)(AIController::GenerateRandomNum(1, 3));
// Add the gene to the Chromosome
returnChromosome->SetGeneAt(randomIndex1, tp);
tp.x = AIController::GenerateRandomNum(0, 25);
tp.y = AIController::GenerateRandomNum(0, 17);
tp.towerType = (TowerType)(AIController::GenerateRandomNum(1, 3));
returnChromosome->SetGeneAt(randomIndex2, tp);
return returnChromosome;
}
}
Chromosome* Population::RankSelection()
{
Chromosome* selectedChromosome = new Chromosome();
int maxWheelSize = 0;
for (int i = 0; i < m_Chromosomes.size(); i++)
{
maxWheelSize += m_Chromosomes[i]->GetRank();
}
int selectedNum = AIController::GenerateRandomNum(1, maxWheelSize-1);
int wheelTotal = 0;
for (int i = 0; i < m_Chromosomes.size(); i++)
{
wheelTotal += m_Chromosomes[i]->GetRank();
if (wheelTotal > selectedNum)
{
selectedChromosome = m_Chromosomes[i];
break;
}
}
return selectedChromosome;
}
I am happy to go into further detail for this or any projects on my portfolio if needs be. I'm unsure of the rights to the games we were given to work on therefore I have not made a public github repo at this time, however if needs be i am more than happy to distribute just my code for the algorithms