/* * ORXONOX - the hottest 3D action shooter ever to exist * > www.orxonox.net < * * * License notice: * * 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 2 * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * Author: * Fabian 'x3n' Landau * Co-authors: * ... * */ /** @file PongAI.cc @brief Implementation of the PongAI class. */ #include "PongAI.h" #include "core/CoreIncludes.h" #include "core/config/ConfigValueIncludes.h" #include "core/command/Executor.h" #include "tools/Timer.h" #include "worldentities/ControllableEntity.h" #include "PongBall.h" namespace orxonox { RegisterUnloadableClass(PongAI); const static float MAX_REACTION_TIME = 0.4f; /** @brief Constructor. Registers and initializes the object. */ PongAI::PongAI(Context* context) : Controller(context) { RegisterObject(PongAI); this->ball_ = nullptr; this->ballDirection_ = Vector2::ZERO; this->ballEndPosition_ = 0; this->randomOffset_ = 0; this->bChangedRandomOffset_ = false; this->relHysteresisOffset_ = 0.02f; this->strength_ = 0.5f; this->movement_ = 0; this->oldMove_ = 0; this->bOscillationAvoidanceActive_ = false; this->setConfigValues(); } /** @brief Destructor. Cleans up the list fo reaction timers. */ PongAI::~PongAI() { for (std::pair& pair : this->reactionTimers_) delete pair.first; } /** @brief Sets config values. */ void PongAI::setConfigValues() { // Sets the strength of the PongAi as a config value. SetConfigValue(strength_, 0.5).description("A value from 0 to 1 where 0 is weak and 1 is strong."); } /** @brief Is called each tick. Implements the behavior of the PongAI (i.e. its intelligence). @param dt The time that has elapsed since the last tick. */ void PongAI::tick(float dt) { // If either the ball, or the controllable entity (i.e. the bat) don't exist (or aren't set). if (this->ball_ == nullptr || this->getControllableEntity() == nullptr) return; Vector3 mypos = this->getControllableEntity()->getPosition(); Vector3 ballpos = this->ball_->getPosition(); Vector3 ballvel = this->ball_->getVelocity(); float hysteresisOffset = this->relHysteresisOffset_ * this->ball_->getFieldDimension().y; char move = 0; bool delay = false; // Check in which direction the ball is flying if ((mypos.x > 0 && ballvel.x < 0) || (mypos.x < 0 && ballvel.x > 0)) { // The ball is flying away this->ballDirection_.x = -1; this->ballDirection_.y = 0; this->bOscillationAvoidanceActive_ = false; // Move to the middle if (mypos.z > hysteresisOffset) move = 1; else if (mypos.z < -hysteresisOffset) move = -1; } else if (ballvel.x == 0) { // The ball is standing still this->ballDirection_.x = 0; this->ballDirection_.y = 0; this->bOscillationAvoidanceActive_ = false; } else { // The ball is approaching if (this->ballDirection_.x != 1) { // The ball just started to approach, initialize all values this->ballDirection_.x = 1; this->ballDirection_.y = sgn(ballvel.z); this->ballEndPosition_ = 0; this->randomOffset_ = 0; this->bChangedRandomOffset_ = false; this->calculateRandomOffset(); this->calculateBallEndPosition(); delay = true; this->bOscillationAvoidanceActive_ = false; } if (this->ballDirection_.y != sgn(ballvel.z)) { // The ball just bounced from a bound, recalculate the predicted end position this->ballDirection_.y = sgn(ballvel.z); this->calculateBallEndPosition(); delay = true; this->bOscillationAvoidanceActive_ = false; } // If the ball is close enough, calculate another random offset to accelerate the ball if (!this->bChangedRandomOffset_) { float timetohit = (-ballpos.x + this->ball_->getFieldDimension().x / 2 * sgn(ballvel.x)) / ballvel.x; if (timetohit < 0.05) { this->bChangedRandomOffset_ = true; if (rnd() < this->strength_) this->calculateRandomOffset(); } } // Move to the predicted end position with an additional offset (to hit the ball with the side of the bat) if (!this->bOscillationAvoidanceActive_) { float desiredZValue = this->ballEndPosition_ + this->randomOffset_; if (mypos.z > desiredZValue + hysteresisOffset * (this->randomOffset_ < 0)) move = 1; else if (mypos.z < desiredZValue - hysteresisOffset * (this->randomOffset_ > 0)) move = -1; } if (move != 0 && this->oldMove_ != 0 && move != this->oldMove_ && !delay) { // We had to correct our position because we moved too far // (and delay is false, so we're not in the wrong place because of a new end-position prediction) if (fabs(mypos.z - this->ballEndPosition_) < 0.5 * this->ball_->getBatLength() * this->ball_->getFieldDimension().y) { // We're not at the right position, but we still hit the ball, so just stay there to avoid oscillation move = 0; this->bOscillationAvoidanceActive_ = true; } } } this->oldMove_ = move; this->move(move, delay); this->getControllableEntity()->moveFrontBack(this->movement_); } /** @brief Calculates the random offset, that accounts for random errors the AI makes in order to be beatable. The higher the strength of the AI, the smaller the (expected value of the) error. The result of this method is stored in this->randomOffset_. */ void PongAI::calculateRandomOffset() { // Calculate the exponent for the position-formula float exp = pow(10.0f, 1.0f - 2.0f*this->strength_); // strength: 0 -> exp = 10 // strength: 0.5 -> exp = 1 // strength: 1 -> exp = 0.1 // Calculate the relative position where to hit the ball with the bat float position = pow(rnd(), exp); // exp > 1 -> position is more likely a small number // exp < 1 -> position is more likely a large number // The position shouldn't be larger than 0.5 (50% of the bat-length from the middle is the end) position *= 0.48f; // Both sides are equally probable position *= rndsgn(); // Calculate the offset in world units this->randomOffset_ = position * this->ball_->getBatLength() * this->ball_->getFieldDimension().y; } /** @brief Calculate the end position the ball will be in. The result of this calculation is stored in this->ballEndPosition_. */ void PongAI::calculateBallEndPosition() { Vector3 position = this->ball_->getPosition(); Vector3 velocity = this->ball_->getVelocity(); Vector3 acceleration = this->ball_->getAcceleration(); Vector2 dimension = this->ball_->getFieldDimension(); // Calculate bounces. The number of predicted bounces is limited by the AIs strength for (float limit = -0.05f; limit < this->strength_ || this->strength_ > 0.99f; limit += 0.4f) { // calculate the time until the ball reaches the other side float totaltime = (-position.x + dimension.x / 2 * sgn(velocity.x)) / velocity.x; // calculate wall bounce position (four possible solutions of the equation: pos.z + vel.z*t + acc.z/2*t^2 = +/- dim.z/2) float bouncetime = totaltime; bool bUpperWall = false; if (acceleration.z == 0) { if (velocity.z > 0) { bUpperWall = true; bouncetime = (dimension.y/2 - position.z) / velocity.z; } else if (velocity.z < 0) { bUpperWall = false; bouncetime = (-dimension.y/2 - position.z) / velocity.z; } } else { // upper wall float temp = velocity.z*velocity.z + 2*acceleration.z*(dimension.y/2 - position.z); if (temp >= 0) { float t1 = (sqrt(temp) - velocity.z) / acceleration.z; float t2 = (sqrt(temp) + velocity.z) / acceleration.z * (-1); if (t1 > 0 && t1 < bouncetime) { bouncetime = t1; bUpperWall = true; } if (t2 > 0 && t2 < bouncetime) { bouncetime = t2; bUpperWall = true; } } // lower wall temp = velocity.z*velocity.z - 2*acceleration.z*(dimension.y/2 + position.z); if (temp >= 0) { float t1 = (sqrt(temp) - velocity.z) / acceleration.z; float t2 = (sqrt(temp) + velocity.z) / acceleration.z * (-1); if (t1 > 0 && t1 < bouncetime) { bouncetime = t1; bUpperWall = false; } if (t2 > 0 && t2 < bouncetime) { bouncetime = t2; bUpperWall = false; } } } if (bouncetime < totaltime) { // Calculate a random prediction error, based on the vertical speed of the ball and the strength of the AI float randomErrorX = rnd(-1, 1) * dimension.y * (velocity.z / velocity.x / PongBall::MAX_REL_Z_VELOCITY) * (1 - this->strength_); float randomErrorZ = rnd(-1, 1) * dimension.y * (velocity.z / velocity.x / PongBall::MAX_REL_Z_VELOCITY) * (1 - this->strength_); // ball bounces after seconds, update the position and continue velocity.z = velocity.z + acceleration.z * bouncetime; if (bUpperWall) { position.z = dimension.y / 2; velocity.z = -fabs(velocity.z) + fabs(randomErrorZ); } else { position.z = -dimension.y / 2; velocity.z = fabs(velocity.z) - fabs(randomErrorZ); } position.x = position.x + velocity.x * bouncetime + randomErrorX; this->ballEndPosition_ = position.z; } else { // ball doesn't bounce, calculate the end position and return // calculate end-height: current height + slope * distance incl. acceleration this->ballEndPosition_ = position.z + velocity.z * totaltime + acceleration.z / 2 * totaltime * totaltime; return; } } } /** @brief Determine the movement the AI will undertake. (Either -1, 0 or 1) The result of this calculation is stored in this->movement_; @param direction The current direction of movement. @param bUseDelay The time by which this move is delayed. (Again, to make the AI less efficient) */ void PongAI::move(char direction, bool bUseDelay) { // The current direction is either what we're doing right now (movement_) or what is last in the queue char currentDirection = this->movement_; if (this->reactionTimers_.size() > 0) currentDirection = this->reactionTimers_.back().second; // Only add changes of direction if (direction == currentDirection) return; if (bUseDelay) { // Calculate delay float delay = MAX_REACTION_TIME * (1 - this->strength_); // Add a new Timer Timer* timer = new Timer(delay, false, createExecutor(createFunctor(&PongAI::delayedMove, this))); this->reactionTimers_.emplace_back(timer, direction); } else { this->movement_ = direction; } } /** @brief Is called, when a delayed move takes effect. */ void PongAI::delayedMove() { // Get the new movement direction from the timer list this->movement_ = this->reactionTimers_.front().second; // Destroy the timer and remove it from the list delete this->reactionTimers_.front().first; this->reactionTimers_.pop_front(); } }