Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions include/ai_patterns.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
Biplanes Revival
Copyright (C) 2019-2025 Regular-dev community
https://regular-dev.org
regular.dev.org@gmail.com

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#pragma once

#include <include/enums.hpp>
#include <include/timer.hpp>
#include <lib/SDL_Vector.h>

#include <vector>
#include <cstddef>

// Forward declarations
class Plane;

// Use the same DIFFICULTY format as in the rest of the project
using DifficultyType = DIFFICULTY::DIFFICULTY;

// New structure for tracking movement patterns
struct AiPatternTracker
{
static constexpr size_t MAX_POSITIONS = 32;

std::vector<SDL_Vector> mRecentPositions {};
std::vector<float> mRecentDirections {};
Timer mPursuitTimer {0.0f};
Timer mPatternBreakCooldown {0.0f};
bool mIsInCircularPattern {false};
bool mIsStalePursuit {false};
SDL_Vector mLastOpponentPosition {};
float mLastOpponentDirection {};

void update(const Plane& self, const Plane& opponent);
bool isCircularPattern() const;
bool isStalePursuit() const;
bool shouldBreakCircularPattern(const Plane& self, const Plane& opponent) const;
void reset();
SDL_Vector getLastOpponentPosition() const { return mLastOpponentPosition; }
const std::vector<SDL_Vector>& getRecentPositions() const { return mRecentPositions; }
};

// Structure for defining bot "personality"
struct AiPersonality
{
enum class Type {
AGGRESSIVE, // Aggressive - always attacks
DEFENSIVE, // Defensive - prefers to avoid conflicts
BALANCED, // Balanced - mixed strategy
PREDICTIVE // Predictive - uses complex tactics
};

Type type {Type::BALANCED};
float aggressiveness {0.5f}; // 0.0 - very cautious, 1.0 - very aggressive
float predictionWeight {0.5f}; // Weight of prediction in decision making
float riskTolerance {0.5f}; // Risk tolerance
float adaptability {0.5f}; // Ability to adapt to opponent's style

void updateBasedOnDifficulty(DifficultyType difficulty);
bool shouldUseAggressiveStrategy(const Plane& self, const Plane& opponent) const;
bool shouldUseDefensiveStrategy(const Plane& self, const Plane& opponent) const;
float getModifiedAimCone(float baseAimCone) const;
};
4 changes: 4 additions & 0 deletions include/constants.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,10 @@ namespace constants
static constexpr float aimConeEasy {2.f};
static constexpr float shootCooldownEasy {2.f * plane::shootCooldown};

static constexpr float circularPatternThreshold {0.23f};
static constexpr float stalePursuitThreshold {3.0f};
static constexpr float movementPredictionTime {0.15f};

namespace debug
{
static constexpr float dangerMagnitude {2.f * plane::sizeX};
Expand Down
275 changes: 275 additions & 0 deletions src/ai_patterns.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
/*
Biplanes Revival
Copyright (C) 2019-2025 Regular-dev community
https://regular-dev.org
regular.dev.org@gmail.com

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#include <include/ai_patterns.hpp>
#include <include/plane.hpp>
#include <include/constants.hpp>
#include <include/time.hpp>
#include <include/math.hpp>

#include <lib/SDL_Vector.h>

#include <cmath>


void
AiPatternTracker::update(const Plane& self, const Plane& opponent)
{
// Update pattern break cooldown timer
mPatternBreakCooldown.Update();

// Add current opponent position and direction to history
mRecentPositions.push_back({opponent.x(), opponent.y()});
mRecentDirections.push_back(opponent.dir());

// Maintain fixed-size history by removing oldest entries
if (mRecentPositions.size() > MAX_POSITIONS)
{
mRecentPositions.erase(mRecentPositions.begin());
mRecentDirections.erase(mRecentDirections.begin());
}

// Detect if opponent is moving or stationary
// This helps identify when opponent is stuck in a pattern
const auto currentOpponentPos = SDL_Vector{opponent.x(), opponent.y()};
const auto distanceMoved = (currentOpponentPos - mLastOpponentPosition).length();
const auto directionChanged = std::abs(opponent.dir() - mLastOpponentDirection);

// If opponent hasn't moved significantly, increment stale pursuit timer
// This helps detect when we're stuck following the same pattern
if (distanceMoved < 0.01f && directionChanged < 5.0f)
{
mPursuitTimer.Update();
}
else
{
mPursuitTimer.Reset();
}

// Store current position and direction for next frame comparison
mLastOpponentPosition = currentOpponentPos;
mLastOpponentDirection = opponent.dir();

// Update pattern detection flags
mIsInCircularPattern = isCircularPattern();
mIsStalePursuit = isStalePursuit();
}

bool
AiPatternTracker::isCircularPattern() const
{
// Need minimum number of positions to detect a pattern
if (mRecentPositions.size() < 8)
return false;

// Calculate center of mass (centroid) of all opponent positions
SDL_Vector center{0.0f, 0.0f};
for (const auto& pos : mRecentPositions)
{
center.x += pos.x;
center.y += pos.y;
}
center.x /= mRecentPositions.size();
center.y /= mRecentPositions.size();

// Calculate average distance from center
float avgDistance = 0.0f;
for (const auto& pos : mRecentPositions)
{
avgDistance += (pos - center).length();
}
avgDistance /= mRecentPositions.size();

// Calculate variance of distances from center
// Low variance indicates points are roughly equidistant from center (circular pattern)
float variance = 0.0f;
for (const auto& pos : mRecentPositions)
{
const float distance = (pos - center).length();
variance += std::abs(distance - avgDistance);
}
variance /= mRecentPositions.size();

// Use adaptive threshold to prevent rapid switching between pattern states
// This creates hysteresis and makes pattern detection more stable
const float adjustedThreshold = constants::ai::circularPatternThreshold * 2.0f;

// If we were recently in a circular pattern, be more lenient
// This prevents flickering between pattern and non-pattern states
const bool wasRecentlyCircular = mIsInCircularPattern && mRecentPositions.size() >= 6;
const float finalThreshold = wasRecentlyCircular ? adjustedThreshold * 1.5f : adjustedThreshold;

// Pattern is circular if variance is low and average distance is significant
return variance < finalThreshold && avgDistance > 0.05f;
}

bool
AiPatternTracker::isStalePursuit() const
{
// Check if we've been pursuing the same pattern for too long
return mPursuitTimer.remainderTime() < (mPursuitTimer.timeout() - constants::ai::stalePursuitThreshold);
}

bool
AiPatternTracker::shouldBreakCircularPattern(const Plane& self, const Plane& opponent) const
{
if (!mIsInCircularPattern)
return false;

// Calculate center and radius of the circular pattern
SDL_Vector center{0.0f, 0.0f};
for (const auto& pos : mRecentPositions)
{
center.x += pos.x;
center.y += pos.y;
}
center.x /= mRecentPositions.size();
center.y /= mRecentPositions.size();

// Calculate average radius of the pattern
float radius = 0.0f;
for (const auto& pos : mRecentPositions)
{
radius += (pos - center).length();
}
radius /= mRecentPositions.size();

// Check our position relative to the pattern center
const auto selfPos = SDL_Vector{self.x(), self.y()};
const auto distanceToCenter = get_distance_between_points(selfPos, center);

// Multiple conditions for breaking out of circular pattern:

// 1. We're close to the center - good position to break out
const bool closeToCenter = distanceToCenter < radius * 0.8f;

// 2. Pattern has been going on too long
const bool patternTooLong = mPursuitTimer.remainderTime() < (mPursuitTimer.timeout() - constants::ai::stalePursuitThreshold * 0.5f);

// 3. We've been in pattern for more than 0.5 seconds
const bool patternTimeExceeded = mPursuitTimer.remainderTime() < (mPursuitTimer.timeout() - 0.5f);

// 4. Opponent is too close - dangerous situation, need to break away
const auto distanceToOpponent = get_distance_between_points(selfPos, {opponent.x(), opponent.y()});
const bool opponentTooClose = distanceToOpponent < 0.35f;

return closeToCenter || patternTooLong || patternTimeExceeded || opponentTooClose;
}

void
AiPatternTracker::reset()
{
// Clear all pattern tracking data
mRecentPositions.clear();
mRecentDirections.clear();
mPursuitTimer.Reset();
mPatternBreakCooldown.Reset();
mIsInCircularPattern = false;
mIsStalePursuit = false;
}

void
AiPersonality::updateBasedOnDifficulty(DifficultyType difficulty)
{
// Configure personality parameters based on difficulty level
// Each difficulty has unique characteristics that affect AI behavior
switch (difficulty)
{
case DIFFICULTY::EASY:
type = Type::DEFENSIVE;
aggressiveness = 0.6f;
predictionWeight = 0.4f;
riskTolerance = 0.4f;
adaptability = 0.4f;
break;

case DIFFICULTY::MEDIUM:
type = Type::BALANCED;
aggressiveness = 0.8f;
predictionWeight = 0.7f;
riskTolerance = 0.7f;
adaptability = 0.6f;
break;

case DIFFICULTY::HARD:
type = Type::AGGRESSIVE;
aggressiveness = 0.9f;
predictionWeight = 0.9f;
riskTolerance = 0.9f;
adaptability = 0.9f;
break;

case DIFFICULTY::DEVELOPER:
type = Type::PREDICTIVE;
aggressiveness = 1.0f;
predictionWeight = 1.0f;
riskTolerance = 1.0f;
adaptability = 1.0f;
break;
}
}

bool
AiPersonality::shouldUseAggressiveStrategy(const Plane& self, const Plane& opponent) const
{
// Calculate health ratios for tactical decision making
const auto selfHp = static_cast<float>(self.hp()) / constants::plane::maxHp;
const auto opponentHp = static_cast<float>(opponent.hp()) / constants::plane::maxHp;

// Calculate distance to opponent
const auto distance = get_distance_between_points(
{self.x(), self.y()},
{opponent.x(), opponent.y()}
);

// Use aggressive strategy if:
// 1. We have significantly more health than opponent AND are close enough to engage
// 2. OR our personality is naturally very aggressive
return (selfHp > opponentHp * 0.8f && distance < 0.4f) || aggressiveness > 0.7f;
}

bool
AiPersonality::shouldUseDefensiveStrategy(const Plane& self, const Plane& opponent) const
{
// Calculate health ratios for tactical decision making
const auto selfHp = static_cast<float>(self.hp()) / constants::plane::maxHp;
const auto opponentHp = static_cast<float>(opponent.hp()) / constants::plane::maxHp;

// Calculate distance to opponent
const auto distance = get_distance_between_points(
{self.x(), self.y()},
{opponent.x(), opponent.y()}
);

// Use defensive strategy if:
// 1. We have significantly less health than opponent
// 2. OR opponent is dangerously close
// 3. OR our personality is naturally very defensive
return (selfHp < opponentHp * 0.6f || distance < 0.2f) || aggressiveness < 0.3f;
}

float
AiPersonality::getModifiedAimCone(float baseAimCone) const
{
// Adjust aim accuracy based on personality's prediction weight
// Higher prediction weight = more accurate aiming
const float accuracyModifier = 1.0f - (predictionWeight * 0.5f);
return baseAimCone * accuracyModifier;
}
Loading