#include "botinterface.hpp" #include "common.hpp" #include "gamestate.hpp" #include #include #include #include #include #include PlayerDirection lastKnownDirection = kNone; AISnake::AISnake() { ; } PlayerDirection AISnake::GetInput(const sf::Vector2f* source) { if (g_pEngine->state.m_bSmart) return CurrentBestDecision(); sf::Vector2f directionDelta; 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; } 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); } } 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 void AISnake::GetNewPath(const sf::Vector2f& source) { // Search for food // Probability-based approach for fun double roll = ((double) GenerateRandomNumber(RAND_MAX)) / ((double) RAND_MAX); if (roll <= probabilityBFS) { BFS(source); } else { DFS(source); } UnvisitBoard(); if (pathFailed) { pathFailed = false; EmptyPath(); path.push(GetAnyOpenPath(source)); } else { TrimPath(); if (path.empty()) path.push(GetAnyOpenPath(source)); } } void AISnake::BFS(const sf::Vector2f& source) { std::queue search; search.push(source); while (!search.empty()) { sf::Vector2f currentLocation = search.front(); search.pop(); if (g_pEngine->gameBoard.at(currentLocation.y).at(currentLocation.x).m_bVisited) continue; botPathUnsanitized.push(currentLocation); std::array 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 { GameSpace* space = &g_pEngine->gameBoard.at(nearby.y).at(nearby.x); if (space->m_bFood) { botPathUnsanitized.push(nearby); return; } if (nearby.x < 1 || nearby.y < 1) continue; if (space->m_bVisited) continue; if (space->m_bSnake) continue; search.push(nearby); } catch (const std::out_of_range& error) { continue; // Out of bounds } } g_pEngine->gameBoard.at(currentLocation.y).at(currentLocation.x).m_bVisited = true; } pathFailed = true; } void AISnake::DFS(const sf::Vector2f& source) { std::stack search; search.push(source); while (!search.empty()) { sf::Vector2f currentLocation = search.top(); search.pop(); if (g_pEngine->gameBoard.at(currentLocation.y).at(currentLocation.x).m_bVisited) continue; botPathUnsanitized.push(currentLocation); std::array 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 { GameSpace* space = &g_pEngine->gameBoard.at(nearby.y).at(nearby.x); if (space->m_bFood) { botPathUnsanitized.push(nearby); return; } if (nearby.x < 1 || nearby.x > g_pEngine->GetGameBoundaries().x - 2) continue; if (space->m_bVisited) continue; if (space->m_bSnake) continue; search.push(nearby); } catch (const std::out_of_range& error) { continue; // Out of bounds } } g_pEngine->gameBoard.at(currentLocation.y).at(currentLocation.x).m_bVisited = true; } pathFailed = true; } sf::Vector2f AISnake::GetAnyOpenPath(const sf::Vector2f& source) { sf::Vector2f bail; std::array 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 { 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) { for (std::vector& 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) { bool reachedSnake = false; path.push(botPathUnsanitized.top()); // Push food location while (!botPathUnsanitized.empty()) { 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); } botPathUnsanitized.pop(); } } 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 choices; std::array 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; 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; } catch (const std::out_of_range& error) { continue; // Out of bounds } } } chances[i] = openSpaces * 0.11111111111; } probabilityDown *= chances[0] * 4; probabilityRight *= chances[1] * 4; probabilityUp *= chances[2] * 4; probabilityLeft *= chances[3] * 4; } // 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 *= 1.5; if (delta.x < 0) probabilityRight *= 1.5; if (delta.y > 0) probabilityUp *= 1.5; if (delta.y < 0) probabilityDown *= 1.5; } 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; } }