1 /* bzflag
2  * Copyright (c) 1993-2021 Tim Riker
3  *
4  * This package is free software;  you can redistribute it and/or
5  * modify it under the terms of the license found in the file
6  * named COPYING that should have accompanied this file.
7  *
8  * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
9  * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
10  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
11  */
12 
13 /*
14  *
15  */
16 
17 // interface header
18 #include "RobotPlayer.h"
19 
20 // common implementation headers
21 #include "BZDBCache.h"
22 
23 // local implementation headers
24 #include "World.h"
25 #include "Intersect.h"
26 #include "TargetingUtils.h"
27 
28 std::vector<BzfRegion*>* RobotPlayer::obstacleList = NULL;
29 
RobotPlayer(const PlayerId & _id,const char * _name,ServerLink * _server,const char * _motto="")30 RobotPlayer::RobotPlayer(const PlayerId& _id, const char* _name,
31                          ServerLink* _server,
32                          const char* _motto = "") :
33     LocalPlayer(_id, _name, _motto),
34     target(NULL),
35     pathIndex(0),
36     timerForShot(0.0f),
37     drivingForward(true)
38 {
39     gettingSound = false;
40     server       = _server;
41 }
42 
43 // estimate a player's position at now+t, similar to dead reckoning
projectPosition(const Player * targ,const float t,float & x,float & y,float & z) const44 void RobotPlayer::projectPosition(const Player *targ,const float t,float &x,float &y,float &z) const
45 {
46     double hisx=targ->getPosition()[0];
47     double hisy=targ->getPosition()[1];
48     double hisz=targ->getPosition()[2];
49     double hisvx=targ->getVelocity()[0];
50     double hisvy=targ->getVelocity()[1];
51     double hisvz=targ->getVelocity()[2];
52     double omega=fabs(targ->getAngularVelocity());
53     double sx,sy;
54 
55     if ((targ->getStatus() & PlayerState::Falling) || fabs(omega) < 2*M_PI / 360 * 0.5)
56     {
57         sx=t*hisvx;
58         sy=t*hisvy;
59     }
60     else
61     {
62         double hisspeed = hypotf(hisvx, hisvy);
63         double alfa = omega * t;
64         double r = hisspeed / fabs(omega);
65         double dx = r * sin(alfa);
66         double dy2 = r * (1 - cos(alfa));
67         double beta = atan2(dy2, dx) * (targ->getAngularVelocity() > 0 ? 1 : -1);
68         double gamma = atan2(hisvy, hisvx);
69         double rho = gamma+beta;
70         sx = hisspeed * t * cos(rho);
71         sy = hisspeed * t * sin(rho);
72     }
73     x=(float)hisx+(float)sx;
74     y=(float)hisy+(float)sy;
75     z=(float)hisz+(float)hisvz*t;
76     if (targ->getStatus() & PlayerState::Falling)
77         z += 0.5f * BZDBCache::gravity * t * t;
78     if (z < 0) z = 0;
79 }
80 
81 
82 // get coordinates to aim at when shooting a player; steps:
83 // 1. estimate how long it will take shot to hit target
84 // 2. calc position of target at that point of time
85 // 3. jump to 1., using projected position, loop until result is stable
getProjectedPosition(const Player * targ,float * projpos) const86 void RobotPlayer::getProjectedPosition(const Player *targ, float *projpos) const
87 {
88     double myx = getPosition()[0];
89     double myy = getPosition()[1];
90     double hisx = targ->getPosition()[0];
91     double hisy = targ->getPosition()[1];
92     double deltax = hisx - myx;
93     double deltay = hisy - myy;
94     double distance = hypotf(deltax,deltay) - BZDB.eval(StateDatabase::BZDB_MUZZLEFRONT) - BZDBCache::tankRadius;
95     if (distance <= 0) distance = 0;
96     double shotspeed = BZDB.eval(StateDatabase::BZDB_SHOTSPEED)*
97                        (getFlag() == Flags::Laser ? BZDB.eval(StateDatabase::BZDB_LASERADVEL) :
98                         getFlag() == Flags::RapidFire ? BZDB.eval(StateDatabase::BZDB_RFIREADVEL) :
99                         getFlag() == Flags::MachineGun ? BZDB.eval(StateDatabase::BZDB_MGUNADVEL) : 1) +
100                        hypotf(getVelocity()[0], getVelocity()[1]);
101 
102     double errdistance = 1.0;
103     float tx, ty, tz;
104     for (int tries=0 ; errdistance > 0.05 && tries < 4 ; tries++)
105     {
106         float t = (float)distance / (float)shotspeed;
107         projectPosition(targ, t + 0.05f, tx, ty, tz); // add 50ms for lag
108         double distance2 = hypotf(tx - myx, ty - myy);
109         errdistance = fabs(distance2-distance) / (distance + ZERO_TOLERANCE);
110         distance = distance2;
111     }
112     projpos[0] = tx;
113     projpos[1] = ty;
114     projpos[2] = tz;
115 
116     // projected pos in building -> use current pos
117     if (World::getWorld()->inBuilding(projpos, 0.0f, BZDBCache::tankHeight))
118     {
119         projpos[0] = targ->getPosition()[0];
120         projpos[1] = targ->getPosition()[1];
121         projpos[2] = targ->getPosition()[2];
122     }
123 }
124 
125 
doUpdate(float dt)126 void            RobotPlayer::doUpdate(float dt)
127 {
128     LocalPlayer::doUpdate(dt);
129 
130     float tankRadius = BZDBCache::tankRadius;
131     const float shotRange  = BZDB.eval(StateDatabase::BZDB_SHOTRANGE);
132     const float shotRadius = BZDB.eval(StateDatabase::BZDB_SHOTRADIUS);
133 
134     // fire shot if any available
135     timerForShot  -= dt;
136     if (timerForShot < 0.0f)
137         timerForShot = 0.0f;
138 
139     if (getFiringStatus() != Ready)
140         return;
141 
142     bool  shoot   = false;
143     const float azimuth = getAngle();
144     // Allow shooting only if angle is near and timer has elapsed
145     if ((!path.empty()) && timerForShot <= 0.0f)
146     {
147         float p1[3];
148         getProjectedPosition(target, p1);
149         const float* p2     = getPosition();
150         float shootingAngle = atan2f(p1[1] - p2[1], p1[0] - p2[0]);
151         if (shootingAngle < 0.0f)
152             shootingAngle += (float)(2.0 * M_PI);
153         float azimuthDiff   = shootingAngle - azimuth;
154         if (azimuthDiff > M_PI)
155             azimuthDiff -= (float)(2.0 * M_PI);
156         else if (azimuthDiff < -M_PI)
157             azimuthDiff += (float)(2.0 * M_PI);
158 
159         const float targetdistance = hypotf(p1[0] - p2[0], p1[1] - p2[1]) -
160                                      BZDB.eval(StateDatabase::BZDB_MUZZLEFRONT) - tankRadius;
161 
162         const float missby = fabs(azimuthDiff) *
163                              (targetdistance - BZDBCache::tankLength);
164         // only shoot if we miss by less than half a tanklength and no building inbetween
165         if (missby < 0.5f * BZDBCache::tankLength &&
166                 p1[2] < shotRadius)
167         {
168             float pos[3] = {getPosition()[0], getPosition()[1],
169                             getPosition()[2] +  BZDB.eval(StateDatabase::BZDB_MUZZLEHEIGHT)
170                            };
171             float dir[3] = {cosf(azimuth), sinf(azimuth), 0.0f};
172             Ray tankRay(pos, dir);
173             float maxdistance = targetdistance;
174             if (!ShotStrategy::getFirstBuilding(tankRay, -0.5f, maxdistance))
175             {
176                 shoot=true;
177                 // try to not aim at teammates
178                 for (int i=0; i <= World::getWorld()->getCurMaxPlayers(); i++)
179                 {
180                     Player *p = 0;
181                     if (i < World::getWorld()->getCurMaxPlayers())
182                         p = World::getWorld()->getPlayer(i);
183                     else
184                         p = LocalPlayer::getMyTank();
185                     if (!p || p->getId() == getId() || validTeamTarget(p) ||
186                             !p->isAlive()) continue;
187                     float relpos[3] = {getPosition()[0] - p->getPosition()[0],
188                                        getPosition()[1] - p->getPosition()[1],
189                                        getPosition()[2] - p->getPosition()[2]
190                                       };
191                     Ray ray(relpos, dir);
192                     float impact = rayAtDistanceFromOrigin(ray, 5 * BZDBCache::tankRadius);
193                     if (impact > 0 && impact < shotRange)
194                     {
195                         shoot=false;
196                         timerForShot = 0.1f;
197                         break;
198                     }
199                 }
200                 if (shoot && fireShot())
201                 {
202                     // separate shot by 0.2 - 0.8 sec (experimental value)
203                     timerForShot = float(bzfrand()) * 0.6f + 0.2f;
204                 }
205             }
206         }
207     }
208 }
209 
doUpdateMotion(float dt)210 void            RobotPlayer::doUpdateMotion(float dt)
211 {
212     if (isAlive())
213     {
214         bool evading = false;
215         // record previous position
216         const float oldAzimuth = getAngle();
217         const float* oldPosition = getPosition();
218         float position[3];
219         position[0] = oldPosition[0];
220         position[1] = oldPosition[1];
221         position[2] = oldPosition[2];
222         float azimuth = oldAzimuth;
223         float tankAngVel = BZDB.eval(StateDatabase::BZDB_TANKANGVEL);
224         float tankSpeed = BZDBCache::tankSpeed;
225 
226 
227         // basically a clone of Roger's evasive code
228         for (int t=0; t <= World::getWorld()->getCurMaxPlayers(); t++)
229         {
230             Player *p = 0;
231             if (t < World::getWorld()->getCurMaxPlayers())
232                 p = World::getWorld()->getPlayer(t);
233             else
234                 p = LocalPlayer::getMyTank();
235             if (!p || p->getId() == getId())
236                 continue;
237             const int maxShots = p->getMaxShots();
238             for (int s = 0; s < maxShots; s++)
239             {
240                 ShotPath* shot = p->getShot(s);
241                 if (!shot || shot->isExpired())
242                     continue;
243                 // ignore invisible bullets completely for now (even when visible)
244                 if (shot->getFlag() == Flags::InvisibleBullet)
245                     continue;
246 
247                 const float* shotPos = shot->getPosition();
248                 if ((fabs(shotPos[2] - position[2]) > BZDBCache::tankHeight) && (shot->getFlag() != Flags::GuidedMissile))
249                     continue;
250                 const float dist = TargetingUtils::getTargetDistance(position, shotPos);
251                 if (dist < 150.0f)
252                 {
253                     const float *shotVel = shot->getVelocity();
254                     float shotAngle = atan2f(shotVel[1], shotVel[0]);
255                     float shotUnitVec[2] = {cosf(shotAngle), sinf(shotAngle)};
256 
257                     float trueVec[2] = {(position[0]-shotPos[0])/dist,(position[1]-shotPos[1])/dist};
258                     float dotProd = trueVec[0]*shotUnitVec[0]+trueVec[1]*shotUnitVec[1];
259 
260                     if (dotProd > 0.97f)
261                     {
262                         float rotation;
263                         float rotation1 = (float)((shotAngle + M_PI/2.0) - azimuth);
264                         if (rotation1 < -1.0f * M_PI) rotation1 += (float)(2.0 * M_PI);
265                         if (rotation1 > 1.0f * M_PI) rotation1 -= (float)(2.0 * M_PI);
266 
267                         float rotation2 = (float)((shotAngle - M_PI/2.0) - azimuth);
268                         if (rotation2 < -1.0f * M_PI) rotation2 += (float)(2.0 * M_PI);
269                         if (rotation2 > 1.0f * M_PI) rotation2 -= (float)(2.0 * M_PI);
270 
271                         if (fabs(rotation1) < fabs(rotation2))
272                             rotation = rotation1;
273                         else
274                             rotation = rotation2;
275                         setDesiredSpeed(1.0f);
276                         setDesiredAngVel(rotation);
277                         evading = true;
278                     }
279                 }
280             }
281         }
282 
283         // when we are not evading, follow the path
284         if (!evading && dt > 0.0 && pathIndex < (int)path.size())
285         {
286             float distance;
287             float v[2];
288             const float* endPoint = path[pathIndex].get();
289             // find how long it will take to get to next path segment
290             v[0] = endPoint[0] - position[0];
291             v[1] = endPoint[1] - position[1];
292             distance = hypotf(v[0], v[1]);
293             float tankRadius = BZDBCache::tankRadius;
294             // smooth path a little by turning early at corners, might get us stuck, though
295             if (distance <= 2.5f * tankRadius)
296                 pathIndex++;
297 
298             float segmentAzimuth = atan2f(v[1], v[0]);
299             float azimuthDiff = segmentAzimuth - azimuth;
300             if (azimuthDiff > M_PI) azimuthDiff -= (float)(2.0 * M_PI);
301             else if (azimuthDiff < -M_PI) azimuthDiff += (float)(2.0 * M_PI);
302             if (fabs(azimuthDiff) > 0.01f)
303             {
304                 // drive backward when target is behind, try to stick to last direction
305                 if (drivingForward)
306                     drivingForward = fabs(azimuthDiff) < M_PI/2*0.9 ? true : false;
307                 else
308                     drivingForward = fabs(azimuthDiff) < M_PI/2*0.3 ? true : false;
309                 setDesiredSpeed(drivingForward ? 1.0f : -1.0f);
310                 // set desired turn speed
311                 if (azimuthDiff >= dt * tankAngVel)
312                     setDesiredAngVel(1.0f);
313                 else if (azimuthDiff <= -dt * tankAngVel)
314                     setDesiredAngVel(-1.0f);
315                 else
316                     setDesiredAngVel(azimuthDiff / dt / tankAngVel);
317             }
318             else
319             {
320                 drivingForward = true;
321                 // tank doesn't turn while moving forward
322                 setDesiredAngVel(0.0f);
323                 // find how long it will take to get to next path segment
324                 if (distance <= dt * tankSpeed)
325                 {
326                     pathIndex++;
327                     // set desired speed
328                     setDesiredSpeed(distance / dt / tankSpeed);
329                 }
330                 else
331                     setDesiredSpeed(1.0f);
332             }
333         }
334     }
335     LocalPlayer::doUpdateMotion(dt);
336 }
337 
explodeTank()338 void            RobotPlayer::explodeTank()
339 {
340     LocalPlayer::explodeTank();
341     target = NULL;
342     path.clear();
343 }
344 
restart(const float * pos,float _azimuth)345 void            RobotPlayer::restart(const float* pos, float _azimuth)
346 {
347     LocalPlayer::restart(pos, _azimuth);
348     // no target
349     path.clear();
350     target = NULL;
351     pathIndex = 0;
352 
353 }
354 
getTargetPriority(const Player * _target) const355 float           RobotPlayer::getTargetPriority(const
356         Player* _target) const
357 {
358     // don't target teammates or myself
359     if (!this->validTeamTarget(_target))
360         return 0.0f;
361 
362     // go after closest player
363     // FIXME -- this is a pretty stupid heuristic
364     const float worldSize = BZDBCache::worldSize;
365     const float* p1 = getPosition();
366     const float* p2 = _target->getPosition();
367 
368     float basePriority = 1.0f;
369     // give bonus to non-paused player
370     if (!_target->isPaused())
371         basePriority += 2.0f;
372     // give bonus to non-deadzone targets
373     if (obstacleList)
374     {
375         float nearest[2];
376         const BzfRegion* targetRegion = findRegion (p2, nearest);
377         if (targetRegion && targetRegion->isInside(p2))
378             basePriority += 1.0f;
379     }
380     return basePriority
381            - 0.5f * hypotf(p2[0] - p1[0], p2[1] - p1[1]) / worldSize;
382 }
383 
setObstacleList(std::vector<BzfRegion * > * _obstacleList)384 void            RobotPlayer::setObstacleList(std::vector<BzfRegion*>*
385         _obstacleList)
386 {
387     obstacleList = _obstacleList;
388 }
389 
getTarget() const390 const Player*       RobotPlayer::getTarget() const
391 {
392     return target;
393 }
394 
setTarget(const Player * _target)395 void            RobotPlayer::setTarget(const Player* _target)
396 {
397     static int mailbox = 0;
398 
399     path.clear();
400     target = _target;
401     if (!target) return;
402 
403     // work backwards (from target to me)
404     float proj[3];
405     getProjectedPosition(target, proj);
406     const float *p1 = proj;
407     const float* p2 = getPosition();
408     float q1[2], q2[2];
409     BzfRegion* headRegion = findRegion(p1, q1);
410     BzfRegion* tailRegion = findRegion(p2, q2);
411     if (!headRegion || !tailRegion)
412     {
413         // if can't reach target then forget it
414         return;
415     }
416 
417     mailbox++;
418     headRegion->setPathStuff(0.0f, NULL, q1, mailbox);
419     RegionPriorityQueue queue;
420     queue.insert(headRegion, 0.0f);
421     BzfRegion* next;
422     while (!queue.isEmpty() && (next = queue.remove()) != tailRegion)
423         findPath(queue, next, tailRegion, q2, mailbox);
424 
425     // get list of points to go through to reach the target
426     next = tailRegion;
427     do
428     {
429         p1 = next->getA();
430         path.push_back(p1);
431         next = next->getTarget();
432     }
433     while (next && next != headRegion);
434     if (next || tailRegion == headRegion)
435         path.push_back(q1);
436     else
437         path.clear();
438     pathIndex = 0;
439 }
440 
findRegion(const float p[2],float nearest[2]) const441 BzfRegion*      RobotPlayer::findRegion(const float p[2],
442                                         float nearest[2]) const
443 {
444     nearest[0] = p[0];
445     nearest[1] = p[1];
446     const int count = obstacleList->size();
447     for (int o = 0; o < count; o++)
448         if ((*obstacleList)[o]->isInside(p))
449             return (*obstacleList)[o];
450 
451     // point is outside: find nearest region
452     float      distance      = maxDistance;
453     BzfRegion* nearestRegion = NULL;
454     for (int i = 0; i < count; i++)
455     {
456         float currNearest[2];
457         float currDistance = (*obstacleList)[i]->getDistance(p, currNearest);
458         if (currDistance < distance)
459         {
460             nearestRegion = (*obstacleList)[i];
461             distance = currDistance;
462             nearest[0] = currNearest[0];
463             nearest[1] = currNearest[1];
464         }
465     }
466     return nearestRegion;
467 }
468 
getRegionExitPoint(const float p1[2],const float p2[2],const float a[2],const float targetPoint[2],float mid[2],float & priority)469 float           RobotPlayer::getRegionExitPoint(
470     const float p1[2], const float p2[2],
471     const float a[2], const float targetPoint[2],
472     float mid[2], float& priority)
473 {
474     float b[2];
475     b[0] = targetPoint[0] - a[0];
476     b[1] = targetPoint[1] - a[1];
477     float d[2];
478     d[0] = p2[0] - p1[0];
479     d[1] = p2[1] - p1[1];
480 
481     float vect = d[0] * b[1] - d[1] * b[0];
482     float t    = 0.0f;  // safe value
483     if (fabs(vect) > ZERO_TOLERANCE)
484     {
485         // compute intersection along (p1,d) with (a,b)
486         t = (a[0] * b[1] - a[1] * b[0] - p1[0] * b[1] + p1[1] * b[0]) / vect;
487         if (t > 1.0f)
488             t = 1.0f;
489         else if (t < 0.0f)
490             t = 0.0f;
491     }
492 
493     mid[0] = p1[0] + t * d[0];
494     mid[1] = p1[1] + t * d[1];
495 
496     const float distance = hypotf(a[0] - mid[0], a[1] - mid[1]);
497     // priority is to minimize distance traveled and distance left to go
498     priority = distance + hypotf(targetPoint[0] - mid[0], targetPoint[1] - mid[1]);
499     // return distance traveled
500     return distance;
501 }
502 
findPath(RegionPriorityQueue & queue,BzfRegion * region,BzfRegion * targetRegion,const float targetPoint[2],int mailbox)503 void            RobotPlayer::findPath(RegionPriorityQueue& queue,
504                                       BzfRegion* region,
505                                       BzfRegion* targetRegion,
506                                       const float targetPoint[2],
507                                       int mailbox)
508 {
509     const int numEdges = region->getNumSides();
510     for (int i = 0; i < numEdges; i++)
511     {
512         BzfRegion* neighbor = region->getNeighbor(i);
513         if (!neighbor) continue;
514 
515         const float* p1 = region->getCorner(i).get();
516         const float* p2 = region->getCorner((i+1)%numEdges).get();
517         float mid[2], priority;
518         float total = getRegionExitPoint(p1, p2, region->getA(),
519                                          targetPoint, mid, priority);
520         priority += region->getDistance();
521         if (neighbor == targetRegion)
522             total += hypotf(targetPoint[0] - mid[0], targetPoint[1] - mid[1]);
523         total += region->getDistance();
524         if (neighbor->test(mailbox) || total < neighbor->getDistance())
525         {
526             neighbor->setPathStuff(total, region, mid, mailbox);
527             queue.insert(neighbor, priority);
528         }
529     }
530 }
531 
532 
533 // Local Variables: ***
534 // mode: C++ ***
535 // tab-width: 4 ***
536 // c-basic-offset: 4 ***
537 // indent-tabs-mode: nil ***
538 // End: ***
539 // ex: shiftwidth=4 tabstop=4
540