From 75ba83b6d8640fda3a789ad00ab4b41d2a1e6a31 Mon Sep 17 00:00:00 2001 From: DvaMishkiLapa Date: Sat, 30 Aug 2025 20:34:33 +0200 Subject: [PATCH 1/2] AI: add pattern logic to AI behavior --- include/ai_patterns.hpp | 77 +++++++++++ include/constants.hpp | 3 + src/ai_patterns.cpp | 275 ++++++++++++++++++++++++++++++++++++++++ src/ai_stuff.cpp | 231 ++++++++++++++++++++++++++------- 4 files changed, 541 insertions(+), 45 deletions(-) create mode 100644 include/ai_patterns.hpp create mode 100644 src/ai_patterns.cpp diff --git a/include/ai_patterns.hpp b/include/ai_patterns.hpp new file mode 100644 index 0000000..9bee466 --- /dev/null +++ b/include/ai_patterns.hpp @@ -0,0 +1,77 @@ +/* + 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 . +*/ + +#pragma once + +#include +#include +#include + +#include +#include + +// 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 mRecentPositions {}; + std::vector 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(); +}; + +// 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; +}; diff --git a/include/constants.hpp b/include/constants.hpp index 5cc6739..744e3b7 100644 --- a/include/constants.hpp +++ b/include/constants.hpp @@ -385,6 +385,9 @@ namespace constants static constexpr float aimConeEasy {2.f}; static constexpr float shootCooldownEasy {2.f * plane::shootCooldown}; + static constexpr float circularPatternThreshold {0.2f}; + static constexpr float stalePursuitThreshold {4.0f}; + namespace debug { static constexpr float dangerMagnitude {2.f * plane::sizeX}; diff --git a/src/ai_patterns.cpp b/src/ai_patterns.cpp new file mode 100644 index 0000000..7f83f60 --- /dev/null +++ b/src/ai_patterns.cpp @@ -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 . +*/ + +#include +#include +#include +#include +#include + +#include + +#include + + +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(self.hp()) / constants::plane::maxHp; + const auto opponentHp = static_cast(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(self.hp()) / constants::plane::maxHp; + const auto opponentHp = static_cast(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; +} diff --git a/src/ai_stuff.cpp b/src/ai_stuff.cpp index 94aef41..f11429a 100644 --- a/src/ai_stuff.cpp +++ b/src/ai_stuff.cpp @@ -19,6 +19,7 @@ */ #include +#include #include #include #include @@ -546,6 +547,8 @@ class AiStatePlane : public AiState { protected: AiOscillationFixer mOscillationFixer {}; + AiPatternTracker mPatternTracker {}; + AiPersonality mPersonality {}; public: @@ -566,6 +569,8 @@ AiStatePlane::AiStatePlane( mInterestMap = {constants::plane::directionCount}; mDangerMap = {constants::plane::directionCount}; + mPersonality.updateBasedOnDifficulty(gameState().botDifficulty); + constexpr auto ReactionTimeToWeights = &AiTemperature::Weights::FromTime; auto throttleWeight = ReactionTimeToWeights(0.1f, 0.1f); @@ -639,6 +644,9 @@ AiStatePlane::update( for ( auto& [action, temperature] : mActions ) temperature.set(0.f); + if ( self.isDead() == true ) + mPatternTracker.reset(); + return; } @@ -649,6 +657,36 @@ AiStatePlane::update( mInterestMap.reinit(); mDangerMap.reinit(); + // Update pattern tracker + mPatternTracker.update(self, opponent); + + // Update personality based on current difficulty + mPersonality.updateBasedOnDifficulty(botDifficulty); + + // Calculate positions and distances to opponent in advance + const SDL_Vector pos {self.x(), self.y()}; + const SDL_Vector opponentPos {opponent.pilot.x(), opponent.pilot.y()}; + + const SDL_Vector opponentPosL = opponentPos - SDL_Vector{1.f, 0.f}; + const SDL_Vector opponentPosR = opponentPos + SDL_Vector{1.f, 0.f}; + + const auto opponentDistance = get_distance_between_points(pos, opponentPos); + const auto opponentDistanceL = get_distance_between_points(pos, opponentPosL); + const auto opponentDistanceR = get_distance_between_points(pos, opponentPosR); + + std::multimap opponentPositionsSorted + { + {opponentDistance, opponentPos}, + {opponentDistanceL, opponentPosL}, + {opponentDistanceR, opponentPosR}, + }; + opponentPositionsSorted.erase(--opponentPositionsSorted.end()); + + const auto [opponentShortestDistance, opponentShortestPos] = + *opponentPositionsSorted.begin(); + + + const auto speed = 0.5f * std::clamp( self.speed() * constants::tickRate, plane::maxSpeedBase, @@ -813,29 +851,6 @@ AiStatePlane::update( std::vector actions {}; - const SDL_Vector pos {self.x(), self.y()}; - const SDL_Vector opponentPos {opponent.pilot.x(), opponent.pilot.y()}; - - const SDL_Vector opponentPosL = opponentPos - SDL_Vector{1.f, 0.f}; - const SDL_Vector opponentPosR = opponentPos + SDL_Vector{1.f, 0.f}; - - const auto opponentDistance = get_distance_between_points(pos, opponentPos); - const auto opponentDistanceL = get_distance_between_points(pos, opponentPosL); - const auto opponentDistanceR = get_distance_between_points(pos, opponentPosR); - - - std::multimap opponentPositionsSorted - { - {opponentDistance, opponentPos}, - {opponentDistanceL, opponentPosL}, - {opponentDistanceR, opponentPosR}, - }; - opponentPositionsSorted.erase(--opponentPositionsSorted.end()); - - - const auto [opponentShortestDistance, opponentShortestPos] = - *opponentPositionsSorted.begin(); - const auto dirToOpponentAbsolute = get_angle_to_point( pos, opponentShortestPos ); @@ -856,28 +871,39 @@ AiStatePlane::update( if ( opponent.isDead() == false ) - for ( const auto& [distance, position] : opponentPositionsSorted ) + { + // Advanced pattern recognition and response system + // This system detects when AI is stuck in circular pursuit and responds appropriately + + // Check if we're stuck in circular pursuit or stale pattern + const bool shouldUseInterceptStrategy = + mPatternTracker.isCircularPattern() || + mPatternTracker.isStalePursuit(); + + // Check if we should break out of circular pattern + // This applies to ALL difficulty levels to prevent infinite loops + const bool shouldBreakPattern = mPatternTracker.shouldBreakCircularPattern(self, opponent); + + if ( shouldUseInterceptStrategy && !shouldBreakPattern ) { - const auto dirToTargetAbsolute = get_angle_to_point( - pos, position ); - - const auto dirToTargetRelative = get_angle_relative( - self.dir(), dirToTargetAbsolute ); + // Pattern detected but not breaking - use normal pursuit with enhanced positioning + const auto dirToOpponentAbsolute = get_angle_to_point(pos, {opponent.x(), opponent.y()}); + const auto dirToOpponentRelative = get_angle_relative(self.dir(), dirToOpponentAbsolute); + // Enhanced shooting logic with personality-based accuracy const auto bulletOffset = self.bulletSpawnOffset(); - - const float aimCone = + const float baseAimCone = botDifficulty == DIFFICULTY::EASY ? constants::ai::aimConeEasy : constants::ai::aimConeDefault; + // Personality affects aim accuracy - higher prediction weight = better aim + const float aimCone = mPersonality.getModifiedAimCone(baseAimCone); + + const bool willBulletHit = std::abs(dirToOpponentRelative) <= plane::pitchStep * aimCone; const bool canHitInstantly = opponent.isHit( self.x() + bulletOffset.x, - self.y() + bulletOffset.y ) && - botDifficulty > DIFFICULTY::EASY; - - const bool willBulletHit = - std::abs(dirToTargetRelative) <= plane::pitchStep * aimCone; + self.y() + bulletOffset.y ) && botDifficulty > DIFFICULTY::EASY; if ( willBulletHit == true || canHitInstantly == true ) { @@ -885,20 +911,100 @@ AiStatePlane::update( actions.push_back(AiAction::Shoot); } - const size_t dirIndex = std::round(dirToTargetAbsolute / plane::pitchStep); + // Set interest to opponent's current position (normal pursuit behavior) + const size_t opponentDirIndex = std::round(dirToOpponentAbsolute / plane::pitchStep); + const auto distanceToOpponent = get_distance_between_points(pos, {opponent.x(), opponent.y()}); + mInterestMap[opponentDirIndex % mInterestMap.size()] = distanceToOpponent; - if ( isOpponentBehind == true && - opponent.y() > self.y() && - opponent.speed() < plane::maxSpeedBase ) + // For high difficulty levels, add tactical positioning interest points + // This helps AI find better angles for attack + if ( botDifficulty >= DIFFICULTY::HARD ) { - mInterestMap[5] = 0.01f; - mInterestMap[11] = 0.01f; + // Add interest in perpendicular directions for tactical positioning + const auto perpDir1 = dirToOpponentAbsolute + 45.0f; + const auto perpDir2 = dirToOpponentAbsolute - 45.0f; - continue; + const size_t perpDirIndex1 = std::round(perpDir1 / plane::pitchStep); + const size_t perpDirIndex2 = std::round(perpDir2 / plane::pitchStep); + + mInterestMap[perpDirIndex1 % mInterestMap.size()] = distanceToOpponent * 0.3f; + mInterestMap[perpDirIndex2 % mInterestMap.size()] = distanceToOpponent * 0.3f; } + } + else if (shouldBreakPattern) + { + // Pattern breaking strategy - force AI to change course + // This prevents infinite circular pursuit loops + const auto dirToOpponent = get_angle_to_point(pos, {opponent.x(), opponent.y()}); + const auto oppositeDir = dirToOpponent + 180.0f; + + // Create strong interest in opposite direction to break the pattern + const size_t oppositeDirIndex = std::round(oppositeDir / plane::pitchStep); + mInterestMap[oppositeDirIndex % mInterestMap.size()] = 2.0f; + + // Add interest in perpendicular directions for escape options + const size_t perpDir1Index = std::round((oppositeDir + 90.0f) / plane::pitchStep); + const size_t perpDir2Index = std::round((oppositeDir - 90.0f) / plane::pitchStep); + mInterestMap[perpDir1Index % mInterestMap.size()] = 1.2f; + mInterestMap[perpDir2Index % mInterestMap.size()] = 1.2f; + + // Clear interest in opponent's direction to force pattern break + const size_t opponentDirIndex = std::round(dirToOpponent / plane::pitchStep); + mInterestMap[opponentDirIndex % mInterestMap.size()] = 0.0f; + + // Pattern break cooldown prevents immediate return to pattern + // This is handled automatically by the pattern tracker + } + else + { + // Normal pursuit logic + for ( const auto& [distance, position] : opponentPositionsSorted ) + { + const auto dirToTargetAbsolute = get_angle_to_point( + pos, position ); + + const auto dirToTargetRelative = get_angle_relative( + self.dir(), dirToTargetAbsolute ); + + const auto bulletOffset = self.bulletSpawnOffset(); + + const float baseAimCone = + botDifficulty == DIFFICULTY::EASY + ? constants::ai::aimConeEasy + : constants::ai::aimConeDefault; + + const float aimCone = mPersonality.getModifiedAimCone(baseAimCone); + + const bool canHitInstantly = opponent.isHit( + self.x() + bulletOffset.x, + self.y() + bulletOffset.y ) && + botDifficulty > DIFFICULTY::EASY; + + const bool willBulletHit = + std::abs(dirToTargetRelative) <= plane::pitchStep * aimCone; - mInterestMap[dirIndex % mInterestMap.size()] = distance; + if ( willBulletHit == true || canHitInstantly == true ) + { + if ( opponent.hasJumped() == false || botDifficulty > DIFFICULTY::EASY ) + actions.push_back(AiAction::Shoot); + } + + const size_t dirIndex = std::round(dirToTargetAbsolute / plane::pitchStep); + + if ( isOpponentBehind == true && + opponent.y() > self.y() && + opponent.speed() < plane::maxSpeedBase ) + { + mInterestMap[5] = 0.01f; + mInterestMap[11] = 0.01f; + + continue; + } + + mInterestMap[dirIndex % mInterestMap.size()] = distance; + } } + } const auto maxDanger = SDL_Vector{plane::maxSpeedBoosted, plane::maxSpeedBoosted}.length(); @@ -926,12 +1032,47 @@ AiStatePlane::update( } + + // Personality-based strategy application + // This system modifies AI behavior based on personality traits and tactical situation + if ( mPersonality.shouldUseDefensiveStrategy(self, opponent) ) + { + // Defensive strategy: create danger zone around opponent to avoid engagement + // Used when AI has low health, opponent is stronger, or personality is defensive + const auto dirToOpponentAbsolute = get_angle_to_point(pos, opponentShortestPos); + const size_t opponentDirIndex = std::round(dirToOpponentAbsolute / plane::pitchStep); + + // Create wide danger zone (7 directions) around opponent + // Danger strength inversely proportional to aggressiveness + for ( int i = -3; i <= 3; ++i ) + { + const size_t dangerIndex = (opponentDirIndex + i + mDangerMap.size()) % mDangerMap.size(); + mDangerMap[dangerIndex] += 0.4f * (1.0f - mPersonality.aggressiveness); + } + } + else if ( mPersonality.shouldUseAggressiveStrategy(self, opponent) ) + { + // Aggressive strategy: create interest zone around opponent to encourage engagement + // Used when AI has health advantage, is close to opponent, or personality is aggressive + const auto dirToOpponentAbsolute = get_angle_to_point(pos, opponentShortestPos); + const size_t opponentDirIndex = std::round(dirToOpponentAbsolute / plane::pitchStep); + + // Create focused interest zone (3 directions) around opponent + // Interest strength proportional to aggressiveness + for ( int i = -1; i <= 1; ++i ) + { + const size_t interestIndex = (opponentDirIndex + i + mInterestMap.size()) % mInterestMap.size(); + mInterestMap[interestIndex] += 0.3f * mPersonality.aggressiveness; + } + } + + const bool shouldClimb = opponent.isDead() == true || opponent.protectionRemainder() >= 0.5f * plane::spawnProtectionCooldown; const bool shouldRescue = - botDifficulty > DIFFICULTY::EASY && + botDifficulty > DIFFICULTY::EASY && self.hp() < plane::maxHp && (opponent.isDead() == true || opponent.hasJumped() == true); From 48f07d6ab1fd185cf11152b09b731a7ff895f7f3 Mon Sep 17 00:00:00 2001 From: DvaMishkiLapa Date: Sat, 30 Aug 2025 20:42:33 +0200 Subject: [PATCH 2/2] AI: add simple prediction of enemy behavior for AI --- include/ai_patterns.hpp | 2 + include/constants.hpp | 5 +- src/ai_stuff.cpp | 110 ++++++++++++++++++++++++++-------------- 3 files changed, 77 insertions(+), 40 deletions(-) diff --git a/include/ai_patterns.hpp b/include/ai_patterns.hpp index 9bee466..9b5ac18 100644 --- a/include/ai_patterns.hpp +++ b/include/ai_patterns.hpp @@ -52,6 +52,8 @@ struct AiPatternTracker bool isStalePursuit() const; bool shouldBreakCircularPattern(const Plane& self, const Plane& opponent) const; void reset(); + SDL_Vector getLastOpponentPosition() const { return mLastOpponentPosition; } + const std::vector& getRecentPositions() const { return mRecentPositions; } }; // Structure for defining bot "personality" diff --git a/include/constants.hpp b/include/constants.hpp index 744e3b7..671bdee 100644 --- a/include/constants.hpp +++ b/include/constants.hpp @@ -385,8 +385,9 @@ namespace constants static constexpr float aimConeEasy {2.f}; static constexpr float shootCooldownEasy {2.f * plane::shootCooldown}; - static constexpr float circularPatternThreshold {0.2f}; - static constexpr float stalePursuitThreshold {4.0f}; + static constexpr float circularPatternThreshold {0.23f}; + static constexpr float stalePursuitThreshold {3.0f}; + static constexpr float movementPredictionTime {0.15f}; namespace debug { diff --git a/src/ai_stuff.cpp b/src/ai_stuff.cpp index f11429a..a229a5c 100644 --- a/src/ai_stuff.cpp +++ b/src/ai_stuff.cpp @@ -94,6 +94,72 @@ isPlaneStalling( return plane.y() >= constants::plane::groundCollision - gravity; } +// Enhanced shooting logic with movement prediction +static bool +shouldShootWithPrediction( + const Plane& self, + const Plane& opponent, + const SDL_Vector& pos, + const float dirToTargetRelative, + const DIFFICULTY::DIFFICULTY botDifficulty, + const AiPersonality& personality, + const AiPatternTracker& patternTracker ) +{ + namespace plane = constants::plane; + const auto bulletOffset = self.bulletSpawnOffset(); + const float baseAimCone = + botDifficulty == DIFFICULTY::EASY + ? constants::ai::aimConeEasy + : constants::ai::aimConeDefault; + + // Personality affects aim accuracy - higher prediction weight = better aim + const float aimCone = personality.getModifiedAimCone(baseAimCone); + + // Calculate movement prediction for better accuracy + const auto opponentVelocity = SDL_Vector{ + opponent.x() - patternTracker.getLastOpponentPosition().x, + opponent.y() - patternTracker.getLastOpponentPosition().y + }; + + // Enhanced prediction using multiple points + const float predictionTime = constants::ai::movementPredictionTime; + + // Linear prediction (current velocity) + const auto linearPredictedPos = SDL_Vector{ + opponent.x() + opponentVelocity.x * predictionTime, + opponent.y() + opponentVelocity.y * predictionTime + }; + + // Lead prediction (aim ahead based on opponent's direction) + const auto leadPredictedPos = SDL_Vector{ + opponent.x() + opponentVelocity.x * predictionTime * 1.5f, + opponent.y() + opponentVelocity.y * predictionTime * 1.5f + }; + + // Calculate angles to both predicted positions + const auto dirToLinearPredictedAbsolute = get_angle_to_point(pos, linearPredictedPos); + const auto dirToLinearPredictedRelative = get_angle_relative(self.dir(), dirToLinearPredictedAbsolute); + + const auto dirToLeadPredictedAbsolute = get_angle_to_point(pos, leadPredictedPos); + const auto dirToLeadPredictedRelative = get_angle_relative(self.dir(), dirToLeadPredictedAbsolute); + + // Use current position and both predictions for better accuracy + const bool willHitCurrent = std::abs(dirToTargetRelative) <= plane::pitchStep * aimCone; + const bool willHitLinearPredicted = std::abs(dirToLinearPredictedRelative) <= plane::pitchStep * aimCone * 1.1f; + const bool willHitLeadPredicted = std::abs(dirToLeadPredictedRelative) <= plane::pitchStep * aimCone * 1.3f; + const bool canHitInstantly = opponent.isHit( + self.x() + bulletOffset.x, + self.y() + bulletOffset.y ) && botDifficulty > DIFFICULTY::EASY; + + if ( willHitCurrent == true || willHitLinearPredicted == true || willHitLeadPredicted == true || canHitInstantly == true ) + { + if ( opponent.hasJumped() == false || botDifficulty > DIFFICULTY::EASY ) + return true; + } + + return false; +} + static bool isOpponentCloseBehind( const Plane& opponent, @@ -890,25 +956,10 @@ AiStatePlane::update( const auto dirToOpponentAbsolute = get_angle_to_point(pos, {opponent.x(), opponent.y()}); const auto dirToOpponentRelative = get_angle_relative(self.dir(), dirToOpponentAbsolute); - // Enhanced shooting logic with personality-based accuracy - const auto bulletOffset = self.bulletSpawnOffset(); - const float baseAimCone = - botDifficulty == DIFFICULTY::EASY - ? constants::ai::aimConeEasy - : constants::ai::aimConeDefault; - - // Personality affects aim accuracy - higher prediction weight = better aim - const float aimCone = mPersonality.getModifiedAimCone(baseAimCone); - - const bool willBulletHit = std::abs(dirToOpponentRelative) <= plane::pitchStep * aimCone; - const bool canHitInstantly = opponent.isHit( - self.x() + bulletOffset.x, - self.y() + bulletOffset.y ) && botDifficulty > DIFFICULTY::EASY; - - if ( willBulletHit == true || canHitInstantly == true ) + // Use enhanced shooting logic with movement prediction + if (shouldShootWithPrediction(self, opponent, pos, dirToOpponentRelative, botDifficulty, mPersonality, mPatternTracker)) { - if ( opponent.hasJumped() == false || botDifficulty > DIFFICULTY::EASY ) - actions.push_back(AiAction::Shoot); + actions.push_back(AiAction::Shoot); } // Set interest to opponent's current position (normal pursuit behavior) @@ -966,27 +1017,10 @@ AiStatePlane::update( const auto dirToTargetRelative = get_angle_relative( self.dir(), dirToTargetAbsolute ); - const auto bulletOffset = self.bulletSpawnOffset(); - - const float baseAimCone = - botDifficulty == DIFFICULTY::EASY - ? constants::ai::aimConeEasy - : constants::ai::aimConeDefault; - - const float aimCone = mPersonality.getModifiedAimCone(baseAimCone); - - const bool canHitInstantly = opponent.isHit( - self.x() + bulletOffset.x, - self.y() + bulletOffset.y ) && - botDifficulty > DIFFICULTY::EASY; - - const bool willBulletHit = - std::abs(dirToTargetRelative) <= plane::pitchStep * aimCone; - - if ( willBulletHit == true || canHitInstantly == true ) + // Use enhanced shooting logic with movement prediction + if (shouldShootWithPrediction(self, opponent, pos, dirToTargetRelative, botDifficulty, mPersonality, mPatternTracker)) { - if ( opponent.hasJumped() == false || botDifficulty > DIFFICULTY::EASY ) - actions.push_back(AiAction::Shoot); + actions.push_back(AiAction::Shoot); } const size_t dirIndex = std::round(dirToTargetAbsolute / plane::pitchStep);