Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

source: code/trunk/src/modules/pong/PongAI.cc @ 11071

Last change on this file since 11071 was 11071, checked in by landauf, 8 years ago

merged branch cpp11_v3 back to trunk

  • Property svn:eol-style set to native
File size: 14.0 KB
Line 
1/*
2 *   ORXONOX - the hottest 3D action shooter ever to exist
3 *                    > www.orxonox.net <
4 *
5 *
6 *   License notice:
7 *
8 *   This program is free software; you can redistribute it and/or
9 *   modify it under the terms of the GNU General Public License
10 *   as published by the Free Software Foundation; either version 2
11 *   of the License, or (at your option) any later version.
12 *
13 *   This program is distributed in the hope that it will be useful,
14 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
15 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 *   GNU General Public License for more details.
17 *
18 *   You should have received a copy of the GNU General Public License
19 *   along with this program; if not, write to the Free Software
20 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
21 *
22 *   Author:
23 *      Fabian 'x3n' Landau
24 *   Co-authors:
25 *      ...
26 *
27 */
28
29/**
30    @file PongAI.cc
31    @brief Implementation of the PongAI class.
32*/
33
34#include "PongAI.h"
35
36#include "core/CoreIncludes.h"
37#include "core/config/ConfigValueIncludes.h"
38#include "core/command/Executor.h"
39#include "tools/Timer.h"
40
41#include "worldentities/ControllableEntity.h"
42
43#include "PongBall.h"
44
45namespace orxonox
46{
47    RegisterUnloadableClass(PongAI);
48
49    const static float MAX_REACTION_TIME = 0.4f;
50
51    /**
52    @brief
53        Constructor. Registers and initializes the object.
54    */
55    PongAI::PongAI(Context* context) : Controller(context)
56    {
57        RegisterObject(PongAI);
58
59        this->ball_ = nullptr;
60        this->ballDirection_ = Vector2::ZERO;
61        this->ballEndPosition_ = 0;
62        this->randomOffset_ = 0;
63        this->bChangedRandomOffset_ = false;
64        this->relHysteresisOffset_ = 0.02f;
65        this->strength_ = 0.5f;
66        this->movement_ = 0;
67        this->oldMove_ = 0;
68        this->bOscillationAvoidanceActive_ = false;
69
70        this->setConfigValues();
71    }
72
73    /**
74    @brief
75        Destructor. Cleans up the list fo reaction timers.
76    */
77    PongAI::~PongAI()
78    {
79        for (std::pair<Timer*, char>& pair : this->reactionTimers_)
80            delete pair.first;
81    }
82
83    /**
84    @brief
85        Sets config values.
86    */
87    void PongAI::setConfigValues()
88    {
89        // Sets the strength of the PongAi as a config value.
90        SetConfigValue(strength_, 0.5).description("A value from 0 to 1 where 0 is weak and 1 is strong.");
91    }
92
93    /**
94    @brief
95        Is called each tick.
96        Implements the behavior of the PongAI (i.e. its intelligence).
97    @param dt
98        The time that has elapsed since the last tick.
99    */
100    void PongAI::tick(float dt)
101    {
102        // If either the ball, or the controllable entity (i.e. the bat) don't exist (or aren't set).
103        if (this->ball_  == nullptr || this->getControllableEntity() == nullptr)
104            return;
105
106        Vector3 mypos = this->getControllableEntity()->getPosition();
107        Vector3 ballpos = this->ball_->getPosition();
108        Vector3 ballvel = this->ball_->getVelocity();
109        float hysteresisOffset = this->relHysteresisOffset_ * this->ball_->getFieldDimension().y;
110
111        char move = 0;
112        bool delay = false;
113
114        // Check in which direction the ball is flying
115        if ((mypos.x > 0 && ballvel.x < 0) || (mypos.x < 0 && ballvel.x > 0))
116        {
117            // The ball is flying away
118            this->ballDirection_.x = -1;
119            this->ballDirection_.y = 0;
120            this->bOscillationAvoidanceActive_ = false;
121
122            // Move to the middle
123            if (mypos.z > hysteresisOffset)
124                move = 1;
125            else if (mypos.z < -hysteresisOffset)
126                move = -1;
127        }
128        else if (ballvel.x == 0)
129        {
130            // The ball is standing still
131            this->ballDirection_.x = 0;
132            this->ballDirection_.y = 0;
133            this->bOscillationAvoidanceActive_ = false;
134        }
135        else
136        {
137            // The ball is approaching
138            if (this->ballDirection_.x != 1)
139            {
140                // The ball just started to approach, initialize all values
141                this->ballDirection_.x = 1;
142                this->ballDirection_.y = sgn(ballvel.z);
143                this->ballEndPosition_ = 0;
144                this->randomOffset_ = 0;
145                this->bChangedRandomOffset_ = false;
146
147                this->calculateRandomOffset();
148                this->calculateBallEndPosition();
149                delay = true;
150                this->bOscillationAvoidanceActive_ = false;
151            }
152
153            if (this->ballDirection_.y != sgn(ballvel.z))
154            {
155                // The ball just bounced from a bound, recalculate the predicted end position
156                this->ballDirection_.y = sgn(ballvel.z);
157
158                this->calculateBallEndPosition();
159                delay = true;
160                this->bOscillationAvoidanceActive_ = false;
161            }
162
163            // If the ball is close enough, calculate another random offset to accelerate the ball
164            if (!this->bChangedRandomOffset_)
165            {
166                float timetohit = (-ballpos.x + this->ball_->getFieldDimension().x / 2 * sgn(ballvel.x)) / ballvel.x;
167                if (timetohit < 0.05)
168                {
169                    this->bChangedRandomOffset_ = true;
170                    if (rnd() < this->strength_)
171                        this->calculateRandomOffset();
172                }
173            }
174
175            // Move to the predicted end position with an additional offset (to hit the ball with the side of the bat)
176            if (!this->bOscillationAvoidanceActive_)
177            {
178                float desiredZValue = this->ballEndPosition_ + this->randomOffset_;
179
180                if (mypos.z > desiredZValue + hysteresisOffset * (this->randomOffset_ < 0))
181                    move = 1;
182                else if (mypos.z < desiredZValue - hysteresisOffset * (this->randomOffset_ > 0))
183                    move = -1;
184            }
185
186            if (move != 0 && this->oldMove_ != 0 && move != this->oldMove_ && !delay)
187            {
188                // We had to correct our position because we moved too far
189                // (and delay is false, so we're not in the wrong place because of a new end-position prediction)
190                if (fabs(mypos.z - this->ballEndPosition_) < 0.5 * this->ball_->getBatLength() * this->ball_->getFieldDimension().y)
191                {
192                    // We're not at the right position, but we still hit the ball, so just stay there to avoid oscillation
193                    move = 0;
194                    this->bOscillationAvoidanceActive_ = true;
195                }
196            }
197        }
198
199        this->oldMove_ = move;
200        this->move(move, delay);
201        this->getControllableEntity()->moveFrontBack(this->movement_);
202    }
203
204    /**
205    @brief
206        Calculates the random offset, that accounts for random errors the AI makes in order to be beatable.
207        The higher the strength of the AI, the smaller the (expected value of the) error.
208        The result of this method is stored in this->randomOffset_.
209    */
210    void PongAI::calculateRandomOffset()
211    {
212        // Calculate the exponent for the position-formula
213        float exp = pow(10.0f, 1.0f - 2.0f*this->strength_); // strength: 0   -> exp = 10
214                                                             // strength: 0.5 -> exp = 1
215                                                             // strength: 1   -> exp = 0.1
216
217        // Calculate the relative position where to hit the ball with the bat
218        float position = pow(rnd(), exp); // exp > 1 -> position is more likely a small number
219                                          // exp < 1 -> position is more likely a large number
220
221        // The position shouldn't be larger than 0.5 (50% of the bat-length from the middle is the end)
222        position *= 0.48f;
223
224        // Both sides are equally probable
225        position *= rndsgn();
226
227        // Calculate the offset in world units
228        this->randomOffset_ = position * this->ball_->getBatLength() * this->ball_->getFieldDimension().y;
229    }
230
231    /**
232    @brief
233        Calculate the end position the ball will be in.
234        The result of this calculation is stored in this->ballEndPosition_.
235    */
236    void PongAI::calculateBallEndPosition()
237    {
238        Vector3 position = this->ball_->getPosition();
239        Vector3 velocity = this->ball_->getVelocity();
240        Vector3 acceleration = this->ball_->getAcceleration();
241        Vector2 dimension = this->ball_->getFieldDimension();
242
243        // Calculate bounces. The number of predicted bounces is limited by the AIs strength
244        for (float limit = -0.05f; limit < this->strength_ || this->strength_ > 0.99f; limit += 0.4f)
245        {
246            // calculate the time until the ball reaches the other side
247            float totaltime = (-position.x + dimension.x / 2 * sgn(velocity.x)) / velocity.x;
248
249            // calculate wall bounce position (four possible solutions of the equation: pos.z + vel.z*t + acc.z/2*t^2 = +/- dim.z/2)
250            float bouncetime = totaltime;
251            bool bUpperWall = false;
252
253            if (acceleration.z == 0)
254            {
255                if (velocity.z > 0)
256                {
257                    bUpperWall = true;
258                    bouncetime = (dimension.y/2 - position.z) / velocity.z;
259                }
260                else if (velocity.z < 0)
261                {
262                    bUpperWall = false;
263                    bouncetime = (-dimension.y/2 - position.z) / velocity.z;
264                }
265            }
266            else
267            {
268                // upper wall
269                float temp = velocity.z*velocity.z + 2*acceleration.z*(dimension.y/2 - position.z);
270                if (temp >= 0)
271                {
272                    float t1 = (sqrt(temp) - velocity.z) / acceleration.z;
273                    float t2 = (sqrt(temp) + velocity.z) / acceleration.z * (-1);
274                    if (t1 > 0 && t1 < bouncetime)
275                    {
276                        bouncetime = t1;
277                        bUpperWall = true;
278                    }
279                    if (t2 > 0 && t2 < bouncetime)
280                    {
281                        bouncetime = t2;
282                        bUpperWall = true;
283                    }
284                }
285                // lower wall
286                temp = velocity.z*velocity.z - 2*acceleration.z*(dimension.y/2 + position.z);
287                if (temp >= 0)
288                {
289                    float t1 = (sqrt(temp) - velocity.z) / acceleration.z;
290                    float t2 = (sqrt(temp) + velocity.z) / acceleration.z * (-1);
291                    if (t1 > 0 && t1 < bouncetime)
292                    {
293                        bouncetime = t1;
294                        bUpperWall = false;
295                    }
296                    if (t2 > 0 && t2 < bouncetime)
297                    {
298                        bouncetime = t2;
299                        bUpperWall = false;
300                    }
301                }
302            }
303
304            if (bouncetime < totaltime)
305            {
306                // Calculate a random prediction error, based on the vertical speed of the ball and the strength of the AI
307                float randomErrorX = rnd(-1, 1) * dimension.y * (velocity.z / velocity.x / PongBall::MAX_REL_Z_VELOCITY) * (1 - this->strength_);
308                float randomErrorZ = rnd(-1, 1) * dimension.y * (velocity.z / velocity.x / PongBall::MAX_REL_Z_VELOCITY) * (1 - this->strength_);
309
310                // ball bounces after <bouncetime> seconds, update the position and continue
311                velocity.z = velocity.z + acceleration.z * bouncetime;
312
313                if (bUpperWall)
314                {
315                    position.z = dimension.y / 2;
316                    velocity.z = -fabs(velocity.z) + fabs(randomErrorZ);
317                }
318                else
319                {
320                    position.z = -dimension.y / 2;
321                    velocity.z = fabs(velocity.z) - fabs(randomErrorZ);
322                }
323
324                position.x = position.x + velocity.x * bouncetime + randomErrorX;
325                this->ballEndPosition_ = position.z;
326            }
327            else
328            {
329                // ball doesn't bounce, calculate the end position and return
330                // calculate end-height: current height + slope * distance incl. acceleration
331                this->ballEndPosition_ = position.z + velocity.z * totaltime + acceleration.z / 2 * totaltime * totaltime;
332                return;
333            }
334        }
335    }
336
337    /**
338    @brief
339        Determine the movement the AI will undertake. (Either -1, 0 or 1)
340        The result of this calculation is stored in this->movement_;
341    @param direction
342        The current direction of movement.
343    @param bUseDelay
344        The time by which this move is delayed. (Again, to make the AI less efficient)
345    */
346    void PongAI::move(char direction, bool bUseDelay)
347    {
348        // The current direction is either what we're doing right now (movement_) or what is last in the queue
349        char currentDirection = this->movement_;
350        if (this->reactionTimers_.size() > 0)
351            currentDirection = this->reactionTimers_.back().second;
352
353        // Only add changes of direction
354        if (direction == currentDirection)
355            return;
356
357        if (bUseDelay)
358        {
359            // Calculate delay
360            float delay = MAX_REACTION_TIME * (1 - this->strength_);
361
362            // Add a new Timer
363            Timer* timer = new Timer(delay, false, createExecutor(createFunctor(&PongAI::delayedMove, this)));
364            this->reactionTimers_.emplace_back(timer, direction);
365        }
366        else
367        {
368            this->movement_ = direction;
369        }
370    }
371
372    /**
373    @brief
374        Is called, when a delayed move takes effect.
375    */
376    void PongAI::delayedMove()
377    {
378        // Get the new movement direction from the timer list
379        this->movement_ = this->reactionTimers_.front().second;
380
381        // Destroy the timer and remove it from the list
382        delete this->reactionTimers_.front().first;
383        this->reactionTimers_.pop_front();
384    }
385}
Note: See TracBrowser for help on using the repository browser.