1 /******************************************************************************
2  *  Warmux is a convivial mass murder game.
3  *  Copyright (C) 2001-2011 Warmux Team.
4  *
5  *  This program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2 of the License, or
8  *  (at your option) any later version.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program; if not, write to the Free Software
17  *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
18  ******************************************************************************
19  * A factory for AI strategies. It contains no turn specfic data.
20  *****************************************************************************/
21 
22 #include "ai/ai_idea.h"
23 #include "ai/trajectory.h"
24 #include "character/character.h"
25 #include "game/game.h"
26 #include "game/game_mode.h"
27 #include "map/map.h"
28 #include "map/wind.h"
29 #include "object/objects_list.h"
30 #include "team/team.h"
31 #include "team/macro.h"
32 #include "tool/math_tools.h"
33 #include "weapon/explosion.h"
34 #include "weapon/weapon_launcher.h"
35 #include "weapon/weapons_list.h"
36 #include "weapon/shotgun.h"
37 
38 // This constant defines how how much damage is worth killing one character?
39 // e.g. Killing one Character with 20 health is about the same worth like doing a sum of 120 damage (60 each) to two characters without killing them. Both cases would get a rating of 120 when this constant is 100.
40 const float BONUS_FOR_KILLING_CHARACTER = 100;
41 const float MALUS_PER_UNUSED_DAMGE_POINT = 0.1f;
42 const float MIN_GROUND_BONUS = 0.1f;
43 const float MAX_GROUND_BONUS = 1.0f;
44 const float GROUND_BONUS_RANGE = 2000.0f;
45 // At the time this code has been written the
46 // bazooka did about 30-60 additional damage at 2500 force
47 const float MIN_DAMAGE_PER_FORCE_UNIT = 30.0f/2500.0f;
48 const float MAX_DAMAGE_PER_FORCE_UNIT = 60.0f/2500.0f;
49 
CanUseWeapon(const Weapon * weapon)50 bool AIIdea::CanUseWeapon(const Weapon * weapon)
51 {
52   bool correct_weapon = weapon == &(ActiveTeam().GetWeapon());
53   bool can_change_weapon = ActiveTeam().GetWeapon().CanChangeWeapon()
54     && (Game::GetInstance()->ReadState() == Game::PLAYING);
55   return correct_weapon || (can_change_weapon && weapon->EnoughAmmo());
56 }
57 
CanUseCharacter(const Character & character)58 bool AIIdea::CanUseCharacter(const Character & character)
59 {
60   if (character.IsDead())
61     return false;
62 
63   bool can_change_character = GameMode::GetInstance()->AllowCharacterSelection()
64     && (Game::GetInstance()->ReadState() == Game::PLAYING)
65     && !Game::GetInstance()->IsCharacterAlreadyChosen();
66   return (character.IsActiveCharacter() || can_change_character);
67 }
68 
GetDirectionRelativeAngle(LRDirection direction,float angle)69 float AIIdea::GetDirectionRelativeAngle(LRDirection direction, float angle)
70 {
71   return (direction == DIRECTION_LEFT) ? InverseAngleRad(angle) : angle;
72 }
73 
RateDamageDoneToEnemy(int damage,const Character & enemy)74 float AIIdea::RateDamageDoneToEnemy(int damage, const Character & enemy)
75 {
76   float rating = std::min(damage, enemy.GetEnergy());
77   if (damage >= enemy.GetEnergy()) {
78     rating += BONUS_FOR_KILLING_CHARACTER;
79     float unused_damage = damage - enemy.GetEnergy();
80     rating -= MALUS_PER_UNUSED_DAMGE_POINT * unused_damage;
81   }
82   return rating;
83 }
84 
RateDamageDoneToEnemy(int min_damage,int max_damage,const Character & enemy)85 float AIIdea::RateDamageDoneToEnemy(int min_damage, int max_damage, const Character & enemy)
86 {
87   float min_rating = RateDamageDoneToEnemy(min_damage, enemy);
88   float max_rating = RateDamageDoneToEnemy(max_damage, enemy);
89   return (min_rating + max_rating)* 0.5f;
90 }
91 
RateExplosion(const Character & shooter,const Point2i & position,const ExplosiveWeaponConfig & config,const float & expected_additional_distance)92 float AIIdea::RateExplosion(const Character & shooter, const Point2i& position,
93                             const ExplosiveWeaponConfig& config,
94                             const float& expected_additional_distance)
95 {
96   float rating = 0.0f;
97 
98   FOR_ALL_LIVING_CHARACTERS(team, character) {
99     float distance = position.Distance(character->GetCenter());
100     distance += expected_additional_distance;
101     if (distance < 1.0f)
102       distance = 1.0f;
103     Double Dist = distance;
104     float min_damage = GetDamageFromExplosion(config, Dist);
105     float max_damage = min_damage;
106     if (Dist <= config.blast_range) {
107       float force = GetForceFromExplosion(config, Dist).tofloat();
108       min_damage += MIN_DAMAGE_PER_FORCE_UNIT * force;
109       max_damage += MAX_DAMAGE_PER_FORCE_UNIT * force;
110     }
111     bool is_friend = shooter.GetTeamIndex() == character->GetTeamIndex();
112     if (is_friend) {
113       rating -= RateDamageDoneToEnemy(min_damage, max_damage, *character);
114     } else {
115       rating += RateDamageDoneToEnemy(min_damage, max_damage, *character);
116     }
117   }
118   return rating;
119 }
120 
CreateStrategy() const121 AIStrategy * SkipTurnIdea::CreateStrategy() const
122 {
123   const WeaponsList * weapons_list = Game::GetInstance()->GetWeaponsList();
124   const Weapon * weapon = weapons_list->GetWeapon(Weapon::WEAPON_SKIP_TURN);
125   if (!CanUseWeapon(weapon))
126     return NULL;
127   return new SkipTurnStrategy();
128 }
129 
CreateStrategy() const130 AIStrategy * WasteAmmoUnitsIdea::CreateStrategy() const
131 {
132   if (ActiveTeam().GetWeapon().CanChangeWeapon())
133     return NULL;
134   Weapon::Weapon_type weapon_type = ActiveTeam().GetWeapon().GetType();
135   int used_ammo_units = ActiveTeam().ReadNbUnits(weapon_type);
136   float max_angle = -ActiveTeam().GetWeapon().GetMinAngle().tofloat();
137   return new ShootWithGunStrategy(-0.1f, ActiveCharacter(), weapon_type,
138                                   ActiveCharacter().GetDirection(), max_angle, used_ammo_units);
139 }
140 
NoLongerPossible() const141 bool AIShootIdea::NoLongerPossible() const
142 {
143   return shooter.IsDead() || enemy.IsDead();
144 }
145 
GetMaxRating(bool one_shot) const146 float AIShootIdea::GetMaxRating(bool one_shot) const
147 {
148   const WeaponsList *weapons_list = Game::GetInstance()->GetWeaponsList();
149   const WeaponLauncher *weapon = weapons_list->GetWeaponLauncher(weapon_type);
150   int damage = weapon->GetDamage();
151   int units = (one_shot) ? 1 : weapon->ReadInitialNbUnit();
152 
153   if (weapon_type == Weapon::WEAPON_SHOTGUN)
154     damage *= SHOTGUN_BULLETS;
155 
156   return weapons_weighting.GetFactor(weapon_type)*damage*units;
157 }
158 
ShootDirectlyAtEnemyIdea(const WeaponsWeighting & weapons_weighting,const Character & shooter,const Character & enemy,Weapon::Weapon_type weapon_type,int max_distance)159 ShootDirectlyAtEnemyIdea::ShootDirectlyAtEnemyIdea(const WeaponsWeighting & weapons_weighting,
160                                                    const Character & shooter, const Character & enemy,
161                                                    Weapon::Weapon_type weapon_type,
162                                                    int max_distance)
163   : AIShootIdea(weapons_weighting, shooter, enemy, weapon_type)
164   , max_sq_distance(max_distance*max_distance)
165 {
166   // do nothing
167 }
168 
GetObjectAt(const Point2i & pos)169 static const PhysicalObj* GetObjectAt(const Point2i & pos)
170 {
171   const ObjectsList * objects = ObjectsList::GetConstInstance();
172   ObjectsList::const_iterator it = objects->begin();
173   while(it != objects->end()) {
174     const PhysicalObj* object = *it;
175     if (object->GetTestRect().Contains(pos) && !object->IsDead())
176       return object;
177     it++;
178   }
179 
180   FOR_ALL_CHARACTERS(team, character) {
181     if (character->GetTestRect().Contains(pos) && !character->IsDead())
182       return &(*character);
183   }
184 
185   return NULL;
186 }
187 
ObjectLiesOnSegment(const PhysicalObj * object,const Point2i & from,const Point2i & to)188 static bool ObjectLiesOnSegment(const PhysicalObj* object,
189                                 const Point2i& from, const Point2i& to)
190 {
191   const Rectanglei& r = object->GetTestRect();
192   const Point2i& center = object->GetCenter();
193 
194   if (from.y == to.y) {
195     // Horizontal shot
196     return r.Contains(Point2i(center.x, to.y));
197   } else if (from.x == to.x) {
198     // Vertical shot
199     return r.Contains(Point2i(to.x, center.y));
200   }
201 
202   // Are the ordinates within the segment
203   if ((center.x<from.x && center.x<to.x) || (center.x>from.x && center.x>to.x) ||
204       (center.y<from.y && center.y<to.y) || (center.y>from.y && center.y>to.y))
205     return false;
206 
207   // Find point on corresponding line
208   int y = from.y + ((center.x-from.x)*(to.y-from.y))/(to.x-from.x);
209   return r.Contains(Point2i(center.x, y));
210 }
211 
212 
213 /* Returns the object the missile has collided with or NULL if the missile has collided with the ground. */
ShotMisses(const Character * shooter,const Character * enemy,const Point2i & from,const Point2i & to)214 static bool ShotMisses(const Character *shooter, const Character *enemy,
215                        const Point2i& from, const Point2i& to) {
216   Point2i pos = from;
217   Point2i delta = to - from;
218 
219   const ObjectsList * objects = ObjectsList::GetConstInstance();
220   ObjectsList::const_iterator it = objects->begin();
221   while(it != objects->end()) {
222     const PhysicalObj* object = *it;
223     if (!object->IsDead() && ObjectLiesOnSegment(object, from, to))
224       return true;
225     it++;
226   }
227 
228   FOR_ALL_CHARACTERS(team, character) {
229     const PhysicalObj* object = &(*character);
230     if (object!=shooter && object!=enemy && !object->IsDead() &&
231         ObjectLiesOnSegment(object, from, to))
232       return true;
233   }
234 
235   int steps_x = abs(delta.x);
236   int steps_y = abs(delta.y);
237   int step_x = delta.x < 0 ? -1 : 1;
238   int step_y = delta.y < 0 ? -1 : 1;
239   int done_x_mul_steps_y  = 0;
240   int done_y_mul_steps_x  = 0;
241   // explanation of done_x_mul_steps_y:
242   // done_x = how often has step_x been added to pos.x
243   // done_x_mul_steps_y = done_x * steps_y
244   // example of algorithm:
245   // given: departure = (0,0) arrival = (-2,7);
246   // => steps_x = 2; steps_y = 7; step_x = -1; step_y = 1
247   // progress:
248   // pos.x | pos.y | done_x_mul_steps_y | done_y_mul_steps_x
249   // -------------------------------------------------------
250   // 0     | 0     | 0                  | 0
251   // 0     | 1     | 0                  | 2
252   // -1    | 2     | 7                  | 4
253   // -1    | 3     | 7                  | 6
254   // -1    | 4     | 7                  | 8
255   // -1    | 5     | 7                  | 10
256   // -2    | 6     | 14                 | 12
257   // -2    | 7     | 14                 | 14
258   // The algorithm tries to keep the difference small between:
259   // done_x_mul_steps_y and done_y_mul_steps_x.
260   // By doing so it gets assured that all intermediate positions pos form a straight line.
261   // (Or at least something close to a straight line)
262   while (pos != to) {
263     int new_done_x_mul_steps_y = done_x_mul_steps_y + steps_y;
264     int new_done_y_mul_steps_x = done_y_mul_steps_x + steps_x;
265     int diff_after_step_x = abs(done_y_mul_steps_x - new_done_x_mul_steps_y);
266     int diff_after_step_y = abs(new_done_y_mul_steps_x - done_x_mul_steps_y);
267 
268     if (diff_after_step_x <= diff_after_step_y) {
269       pos.x += step_x;
270       done_x_mul_steps_y = new_done_x_mul_steps_y;
271     }
272     if (diff_after_step_y <= diff_after_step_x) {
273       pos.y += step_y;
274       done_y_mul_steps_x = new_done_y_mul_steps_x;
275     }
276 
277     if (GetWorld().IsOutsideWorld(pos))
278       return true;
279 
280     if (!GetWorld().IsInVacuum(pos))
281       return true;
282   }
283   return false;
284 }
285 
CreateStrategy() const286 AIStrategy * ShootDirectlyAtEnemyIdea::CreateStrategy() const {
287   if (enemy.IsDead())
288     return NULL;
289 
290   if (!CanUseCharacter(shooter))
291     return NULL;
292 
293   const WeaponsList *weapons_list = Game::GetInstance()->GetWeaponsList();
294   const WeaponLauncher *weapon = weapons_list->GetWeaponLauncher(weapon_type);
295 
296   if (!CanUseWeapon(weapon))
297     return NULL;
298 
299   // We need to use center point, because gunholePosition is location
300   // of last weapon of the ActiveTeam() and not the future gunholePos
301   // which will be select.
302   // TODO: Please find an alternative to solve this tempory solution
303   Point2i departure = shooter.GetCenter();
304   Point2i arrival = enemy.GetCenter();
305 
306   if (departure.SquareDistance(arrival) > max_sq_distance)
307     return NULL;
308 
309   float original_angle = departure.ComputeAngleFloat(arrival);
310 
311   LRDirection direction = XDeltaToDirection(arrival.x - departure.x);
312   float shoot_angle = GetDirectionRelativeAngle(direction, original_angle);
313 
314   if (!weapon->IsAngleValid(shoot_angle))
315     return NULL;
316 
317   if (ShotMisses(&shooter, &enemy, departure, arrival))
318     return NULL;
319 
320   int available_ammo_units = ActiveTeam().ReadNbUnits(weapon_type);
321 
322   int damage_per_ammo_unit = weapon->GetDamage();
323   if (weapon_type == Weapon::WEAPON_SHOTGUN) {
324     damage_per_ammo_unit *= SHOTGUN_BULLETS;
325   }
326   int required_ammo_units = (enemy.GetEnergy() + damage_per_ammo_unit -1)
327                           / damage_per_ammo_unit;
328   int used_ammo_units = std::min(required_ammo_units, available_ammo_units);
329   int damage = used_ammo_units * damage_per_ammo_unit;
330 
331   float rating = RateDamageDoneToEnemy(damage, enemy);
332   rating = rating * weapons_weighting.GetFactor(weapon_type);
333   return new ShootWithGunStrategy(rating, shooter, weapon_type, direction,
334                                   shoot_angle, used_ammo_units);
335 }
336 
FireMissileWithFixedDurationIdea(const WeaponsWeighting & weapons_weighting,const Character & shooter,const Character & enemy,Weapon::Weapon_type weapon_type,float duration,int timeout)337 FireMissileWithFixedDurationIdea::FireMissileWithFixedDurationIdea(const WeaponsWeighting & weapons_weighting,
338                                                                    const Character & shooter, const Character & enemy,
339                                                                    Weapon::Weapon_type weapon_type,
340                                                                    float duration, int timeout)
341   : AIShootIdea(weapons_weighting, shooter, enemy, weapon_type)
342   , duration(duration)
343   , timeout(timeout)
344 {
345   // do nothing
346 }
347 
IsPositionEmpty(const Character & character_to_ignore,const Point2i & pos,const PhysicalObj ** object)348 static bool IsPositionEmpty(const Character & character_to_ignore,
349                             const Point2i& pos, const PhysicalObj** object)
350 {
351   *object = NULL;
352   if (GetWorld().IsOutsideWorld(pos))
353     return false;
354 
355   if (!GetWorld().IsInVacuum(pos))
356     return false;
357 
358   *object = GetObjectAt(pos);
359   if (*object != NULL && *object != &character_to_ignore)
360     return false;
361   *object = NULL;
362   return true;
363 }
364 
365 #define STEP_IN_PIXEL 2
366 
GetFirstContact(const Character & character_to_ignore,const Trajectory & trajectory,const PhysicalObj ** object)367 static const Point2i GetFirstContact(const Character & character_to_ignore,
368                                      const Trajectory & trajectory,
369                                      const PhysicalObj** object)
370 {
371   float time = 0;
372   Point2i pos;
373   do {
374     pos = trajectory.GetPositionAt(time);
375     float pixel_per_second = trajectory.GetSpeedAt(time);
376     time += STEP_IN_PIXEL / pixel_per_second;
377   } while (IsPositionEmpty(character_to_ignore, pos, object));
378   return pos;
379 }
380 
CreateStrategy() const381 AIStrategy * FireMissileWithFixedDurationIdea::CreateStrategy() const
382 {
383   if (enemy.IsDead())
384     return NULL;
385 
386   if (!CanUseCharacter(shooter))
387     return NULL;
388 
389   const WeaponsList * weapons_list = Game::GetInstance()->GetWeaponsList();
390   const WeaponLauncher * weapon = weapons_list->GetWeaponLauncher(weapon_type);
391 
392   if (!CanUseWeapon(weapon))
393     return NULL;
394   float g = GameMode::GetInstance()->gravity.tofloat();
395   float wind_factor = weapon->GetWindFactor().tofloat();
396   float mass = weapon->GetMass().tofloat();
397   Point2f f(Wind::GetRef().GetStrength().tofloat() * wind_factor, g * mass);
398   Point2f a = f / mass * PIXEL_PER_METER;
399   const Point2f pos_0 = shooter.GetCenter();
400   const Point2f pos_t = enemy.GetCenter();
401   float t = duration;
402   // Calculate v_0 using "pos_t = 1/2 * a_x * t*t + v_0*t + pos_0":
403   Point2f v_0 = (pos_t - pos_0)/t - a/2 * t;
404 
405   float strength = v_0.Norm() / PIXEL_PER_METER;
406   float angle = v_0.ComputeAngleFloat();
407   LRDirection direction = XDeltaToDirection(v_0.x<0);
408   float shoot_angle = GetDirectionRelativeAngle(direction, angle);
409   if (!weapon->IsAngleValid(shoot_angle))
410     return NULL;
411 
412   if (strength > weapon->GetMaxStrength().tofloat())
413     return NULL;
414 
415   Trajectory trajectory(pos_0, v_0, a);
416   const PhysicalObj * aim;
417   Point2i explosion_pos = GetFirstContact(shooter, trajectory, &aim);
418   float rating;
419   bool explodes_on_contact = weapon_type == Weapon::WEAPON_BAZOOKA;
420   if (aim == &enemy || explodes_on_contact) {
421     float expected_additional_distance = explodes_on_contact ? 0.0f : 30.0f;
422     rating = RateExplosion(shooter, explosion_pos, weapon->cfg(), expected_additional_distance);
423 
424     // Explosions remove ground and make it possible to hit the characters behind the ground.
425     // That is why ground hits get rewared with a small positive rating.
426     if (explodes_on_contact) {
427       float distance = explosion_pos.Distance(enemy.GetCenter());
428       // Give more bonus if the explosion is near the target.
429       // This will make the AI focus on one character
430       float ground_bonus = max(MIN_GROUND_BONUS, MAX_GROUND_BONUS - distance/GROUND_BONUS_RANGE);
431       rating += ground_bonus;
432     }
433   } else {
434     return NULL;
435   }
436   rating = rating * weapons_weighting.GetFactor(weapon_type);
437   return new LoadAndFireStrategy(rating, shooter, weapon_type, direction, shoot_angle, strength, timeout);
438 }
439