snakeplusplus/src/botinterface.cpp

353 lines
11 KiB
C++
Raw Normal View History

#include "botinterface.hpp"
#include "common.hpp"
#include "gamestate.hpp"
2023-10-13 19:07:08 -05:00
#include <array>
#include <cstdlib>
#include <iostream>
2023-10-13 19:07:08 -05:00
#include <queue>
#include <stdexcept>
#include <SFML/System/Vector2.hpp>
PlayerDirection lastKnownDirection = kNone;
2023-10-13 19:07:08 -05:00
AISnake::AISnake() {
;
}
2023-10-13 19:07:08 -05:00
PlayerDirection AISnake::GetInput(const sf::Vector2f* source)
{
if (g_pEngine->state.m_bSmart)
return CurrentBestDecision();
sf::Vector2f directionDelta;
2024-08-03 21:18:57 -05:00
if (!source)
return kUp;
while (*source == path.top() && !path.empty()) { path.pop(); }
if (path.empty()) { path.push(GetAnyOpenPath(*source)); }
directionDelta = *source - path.top();
path.pop();
if ((directionDelta.y == 1)
&& (lastKnownDirection != kDown))
{ lastKnownDirection = kUp; }
else if ((directionDelta.y == -1)
&& (lastKnownDirection != kUp))
{ lastKnownDirection = kDown; }
else if ((directionDelta.x == 1)
&& (lastKnownDirection != kRight))
{ lastKnownDirection = kLeft; }
else if ((directionDelta.x == -1)
&& (lastKnownDirection != kLeft))
{ lastKnownDirection = kRight; }
return lastKnownDirection;
}
2023-10-13 19:07:08 -05:00
void AISnake::UpdateProbability(int snakeSize)
{
probabilityBFS = 1 - ((double) snakeSize) / 1000;
return;
}
void AISnake::AdjustProbability(double amount)
{
probabilityBFS += amount;
if (probabilityBFS > 1.0) { probabilityBFS = 1.0; }
if (probabilityBFS < 0.0) { probabilityBFS = 0.0; }
return;
}
void AISnake::AddIteration(const int size)
{
if (size > 40)
{
UpdateAverage(size);
double adjustmentAmount = 0.002;
if (average > size) { AdjustProbability(adjustmentAmount); }
else { AdjustProbability(-adjustmentAmount); }
}
2024-08-03 21:18:57 -05:00
std::cout << "[LOG - AI] Current average: " << average << std::endl;
std::cout << "[LOG - AI] Previous iteration size: " << size << std::endl;
}
void AISnake::ResetPath(void) {
while (!path.empty()) { path.pop(); }
}
// Gets a new path for the bot to follow
// Uses DFS algorithm
2024-08-03 20:42:34 -05:00
void AISnake::GetNewPath(const sf::Vector2f& source)
{
// Search for food
// Probability-based approach for fun
double roll = ((double) GenerateRandomNumber(RAND_MAX)) / ((double) RAND_MAX);
2024-08-03 20:42:34 -05:00
if (roll <= probabilityBFS) { BFS(source); }
else { DFS(source); }
UnvisitBoard();
2024-08-03 20:42:34 -05:00
if (pathFailed) {
pathFailed = false;
EmptyPath();
path.push(GetAnyOpenPath(source));
} else {
TrimPath();
if (path.empty())
path.push(GetAnyOpenPath(source));
}
}
2023-10-13 19:22:11 -05:00
2024-08-03 20:42:34 -05:00
void AISnake::BFS(const sf::Vector2f& source) {
std::queue<sf::Vector2f> search;
search.push(source);
while (!search.empty()) {
sf::Vector2f currentLocation = search.front();
search.pop();
2024-08-03 20:42:34 -05:00
if (g_pEngine->gameBoard.at(currentLocation.y).at(currentLocation.x).m_bVisited)
continue;
botPathUnsanitized.push(currentLocation);
std::array<sf::Vector2f, 4> localLocations;
localLocations.fill(currentLocation);
localLocations[0].y += 1;
localLocations[1].x += 1;
localLocations[2].y -= 1;
localLocations[3].x -= 1;
for (sf::Vector2f nearby : localLocations) {
try {
2024-08-03 20:42:34 -05:00
GameSpace* space = &g_pEngine->gameBoard.at(nearby.y).at(nearby.x);
if (space->m_bFood) {
botPathUnsanitized.push(nearby);
2024-08-03 20:42:34 -05:00
return;
2023-10-13 19:07:08 -05:00
}
2024-08-03 21:18:57 -05:00
if (nearby.x < 1 || nearby.y < 1)
continue;
2024-08-03 20:42:34 -05:00
if (space->m_bVisited)
continue;
2024-08-03 20:42:34 -05:00
if (space->m_bSnake)
continue;
search.push(nearby);
} catch (const std::out_of_range& error) {
continue; // Out of bounds
2023-10-13 19:07:08 -05:00
}
}
g_pEngine->gameBoard.at(currentLocation.y).at(currentLocation.x).m_bVisited = true;
2023-10-13 19:22:11 -05:00
}
2024-08-03 20:42:34 -05:00
pathFailed = true;
}
2023-10-13 19:22:11 -05:00
2024-08-03 20:42:34 -05:00
void AISnake::DFS(const sf::Vector2f& source) {
std::stack<sf::Vector2f> search;
search.push(source);
while (!search.empty()) {
sf::Vector2f currentLocation = search.top();
search.pop();
2024-08-03 20:42:34 -05:00
if (g_pEngine->gameBoard.at(currentLocation.y).at(currentLocation.x).m_bVisited)
continue;
botPathUnsanitized.push(currentLocation);
std::array<sf::Vector2f, 4> localLocations;
localLocations.fill(currentLocation);
localLocations.at(0).y += 1;
localLocations.at(1).x += 1;
localLocations.at(2).y -= 1;
localLocations.at(3).x -= 1;
for (sf::Vector2f nearby : localLocations) {
try {
2024-08-03 20:42:34 -05:00
GameSpace* space = &g_pEngine->gameBoard.at(nearby.y).at(nearby.x);
if (space->m_bFood) {
botPathUnsanitized.push(nearby);
2024-08-03 20:42:34 -05:00
return;
2023-10-13 19:22:11 -05:00
}
2024-08-03 21:18:57 -05:00
if (nearby.x < 1 || nearby.x > g_pEngine->GetGameBoundaries().x - 2)
continue;
2024-08-03 20:42:34 -05:00
if (space->m_bVisited)
continue;
2024-08-03 20:42:34 -05:00
if (space->m_bSnake)
continue;
search.push(nearby);
} catch (const std::out_of_range& error) {
continue; // Out of bounds
2023-10-13 19:22:11 -05:00
}
}
g_pEngine->gameBoard.at(currentLocation.y).at(currentLocation.x).m_bVisited = true;
}
2024-08-03 20:42:34 -05:00
pathFailed = true;
}
2023-10-23 21:33:47 -05:00
sf::Vector2f AISnake::GetAnyOpenPath(const sf::Vector2f& source) {
sf::Vector2f bail;
2024-08-03 20:42:34 -05:00
std::array<sf::Vector2f, 4> paths;
paths.fill(source);
paths[0].x -= 1;
paths[1].x += 1;
paths[2].y -= 1;
paths[3].y += 1;
for (auto path : paths) {
try {
2024-08-03 21:18:57 -05:00
bail = path;
if (g_pEngine->gameBoard.at(path.y).at(path.x).m_bSnake)
continue;
return path;
} catch (const std::out_of_range& error) {
continue; // Out of bounds
}
}
return bail; // Snake is trapped, give up and die
}
void AISnake::UnvisitBoard(void) {
2024-08-03 20:42:34 -05:00
for (std::vector<GameSpace>& i : g_pEngine->gameBoard)
for (GameSpace& j : i)
j.m_bVisited = false;
}
void AISnake::UpdateAverage(const int size) {
totalLength += size;
amountPlayed += 1;
average = (double)totalLength / amountPlayed;
}
void AISnake::TrimPath(void) {
2024-08-03 20:42:34 -05:00
bool reachedSnake = false;
path.push(botPathUnsanitized.top()); // Push food location
while (!botPathUnsanitized.empty()) {
2024-08-03 20:42:34 -05:00
if (!reachedSnake) {
sf::Vector2f location = botPathUnsanitized.top();
if (g_pEngine->gameBoard[location.y][location.x].m_bSnake)
reachedSnake = true;
sf::Vector2f deltaVector = location - path.top();
int delta = abs(deltaVector.x) + abs(deltaVector.y);
if (delta == 1)
path.push(location);
2023-10-13 19:07:08 -05:00
}
botPathUnsanitized.pop();
2023-10-13 19:07:08 -05:00
}
2024-08-03 20:42:34 -05:00
}
void AISnake::EmptyPath(void) {
while (!botPathUnsanitized.empty())
botPathUnsanitized.pop();
}
// Main method for using AI snake
PlayerDirection AISnake::CurrentBestDecision(void) {
// Reset probabilities
probabilityUp = 0.25;
probabilityDown = 0.25;
probabilityLeft = 0.25;
probabilityRight = 0.25;
// Calculate options
CheckLocalFreedom();
CheckFoodDirection();
// Deny impossible movement
RemoveImpossibleChoice();
// Make decision
if (probabilityUp > probabilityDown) {
if (probabilityUp < probabilityLeft)
return kLeft;
if (probabilityUp < probabilityRight)
return kRight;
return kUp;
} else {
if (probabilityDown < probabilityLeft)
return kLeft;
if (probabilityDown < probabilityRight)
return kRight;
return kDown;
}
}
// Improves probability based on amount of local open spaces
// TODO: Add weights
void AISnake::CheckLocalFreedom(void) {
std::array<sf::Vector2f, 4> choices;
std::array<double, 4> chances;
chances.fill(0);
choices.fill(g_pEngine->GetHeadLocation());
choices[0].y += 1;
choices[1].x += 1;
choices[2].y -= 1;
choices[3].x -= 1;
for (int i = 0; i < 4; ++i) {
try {
if (g_pEngine->gameBoard.at(choices[i].y).at(choices[i].x).m_bSnake) {
chances[i] = 0;
continue;
}
if (g_pEngine->gameBoard.at(choices[i].y).at(choices[i].x).m_bFood) {
chances[0] = 0;
chances[1] = 0;
chances[2] = 0;
chances[3] = 0;
chances[i] = 1;
break;
}
} catch (const std::out_of_range& error) {
continue; // Out of bounds
}
double openSpaces = 0;
bool foodNearby = false;
for (int j = -1; j < 2; ++j) {
for (int k = -1; k < 2; ++k) {
try {
if (!g_pEngine->gameBoard.at(choices[i].y + j).at(choices[i].x + k).m_bSnake)
++openSpaces;
if (g_pEngine->gameBoard.at(choices[i].y + j).at(choices[i].x + k).m_bFood)
foodNearby = true;
} catch (const std::out_of_range& error) {
continue; // Out of bounds
}
}
}
chances[i] = openSpaces * 0.11111111111;
if (foodNearby)
chances[i] = 1; // Ignore chance, be greedy because food
}
probabilityDown *= chances[0];
probabilityRight *= chances[1];
probabilityUp *= chances[2];
probabilityLeft *= chances[3];
}
// Improves probability that direction of food is best option
// TODO: Add weights
void AISnake::CheckFoodDirection(void) {
sf::Vector2f delta = g_pEngine->GetHeadLocation() - g_pEngine->GetFoodLocation();
if (delta.x > 0)
probabilityLeft *= 2;
if (delta.x < 0)
probabilityRight *= 2;
if (delta.y > 0)
probabilityUp *= 2;
if (delta.y < 0)
probabilityDown *= 2;
std::array<sf::Vector2f, 4> choices;
choices.fill(g_pEngine->GetHeadLocation());
choices[0].y += 1;
choices[1].x += 1;
choices[2].y -= 1;
choices[3].x -= 1;
}
void AISnake::RemoveImpossibleChoice(void) {
if (g_pEngine->GetPlayerSize() == 1)
return; // Player can go any direction
PlayerDirection currentDirection = g_pEngine->GetCurrentDirection();
switch (currentDirection) {
case (kUp):
probabilityDown = 0;
break;
case (kDown):
probabilityUp = 0;
break;
case (kLeft):
probabilityRight = 0;
break;
case (kRight):
probabilityLeft = 0;
break;
default:
std::cout << "[ERR - AI] Impossibility defaulted somehow??" << std::endl;
break;
}
}