1 /*
2 * Kuklomenos
3 * Copyright (C) 2008-2009 Martin Bays <mbays@sdf.lonestar.org>
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 3 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, see http://www.gnu.org/licenses/.
17 */
18
19 #include "ai.h"
20 #include "random.h"
21 #include "state.h"
22 #include "invaders.h"
23 #include "coords.h"
24 #include "node.h"
25 #include "gfx.h"
26 #include "settings.h"
27
28 /* AI
29 *
30 * Architecture:
31 *
32 * 'update' gets called every time the state gets updated; it should set
33 * 'keys', which will then affect state update just as if the player were
34 * pressing them.
35 *
36 * The AI can peek directly at the state. Because keeping its own copy
37 * of everything would be a nightmare, it can also record whatever
38 * record-keeping information it likes in the 'aidata' attribute of state
39 * objects, which is of type 'struct AIData'.
40 *
41 * XXX: necessarily, the AI code depends heavily on the specifics of the game
42 * rules, with many formulae copied from or based on those in state.cc. Be
43 * sure to keep it in sync.
44 */
45
46 // AI::closestEnemy: return closest shootable enemy
closestEnemy()47 HPInvader* AI::closestEnemy()
48 {
49 HPInvader* closest = NULL;
50 float closestDist = -1;
51
52 for (std::vector<Invader*>::iterator it = gameState->invaders.begin();
53 it != gameState->invaders.end();
54 it++)
55 {
56 HPInvader* inv = dynamic_cast<HPInvader*>(*it);
57 if (inv == NULL)
58 continue;
59
60 float d = dist(inv->cpos() - ARENA_CENTRE);
61 if ( inv->aiData.seen && inv->evil() &&
62 inv->hitsShots() && inv->armour < 3 &&
63 (closest == NULL || d < closestDist) )
64 {
65 closestDist = d;
66 closest = inv;
67 }
68 }
69
70 return closest;
71 }
72
updateSeen()73 void AI::updateSeen()
74 {
75 const RelPolarCoord d(gameState->you.aim.angle, gameState->zoomdist);
76
77 const View zoomView(ARENA_CENTRE + d,
78 (float)screenGeom.rad/((float)ARENA_RAD-gameState->zoomdist),
79 settings.rotatingView ? -d.angle : 0);
80
81 for (std::vector<Invader*>::iterator it = gameState->invaders.begin();
82 it != gameState->invaders.end();
83 it++)
84 if (zoomView.inView((*it)->cpos()))
85 (*it)->aiData.seen=true;
86 }
87
88 // AI::predictPos: return predicted position of 'inv' in 'time' ms
predictPos(Invader * inv,float time)89 RelPolarCoord AI::predictPos(Invader* inv, float time)
90 {
91 SpirallingInvader* spiralInv = dynamic_cast<SpirallingInvader*>(inv);
92 if (spiralInv)
93 {
94 RelPolarCoord pos = spiralInv->pos;
95
96 InfestingInvader* infInv = dynamic_cast<InfestingInvader*>(spiralInv);
97 if (!infInv)
98 {
99 // use same calculation as in SpirallingInvader::doUpdate(int time):
100 const int steps = std::min(10, 1 + (int)(time / 30));
101 for (int i = 0; i < steps; i++)
102 {
103 pos.angle += spiralInv->ds*0.0001*(100/spiralInv->pos.dist)*(time/steps);
104 pos.dist += spiralInv->dd*-0.0075*(time/steps);
105 }
106 }
107 else
108 {
109 // InfestingInvaders are a special case - they align with
110 // their target node
111 Node* node = infInv->targetNode;
112 pos.angle = node->pos.angle +
113 node->ds*0.0001*(100/node->pos.dist)*time;
114 pos.dist += spiralInv->dd*-0.0075*time;
115 }
116 return pos;
117 }
118 else
119 // linear approximation
120 return (inv->cpos()
121 + (inv->collObj().velocity * time)
122 - ARENA_CENTRE
123 );
124 }
125
126 /* BasicAI:
127 * Simple, mostly stateless, reactionary AI.
128 *
129 * 'update' routine:
130 * Consider closest enemy. Estimate, in a simplistic inaccurate way, where we
131 * want to be rotated to so as to give us time to aim sufficiently and fire.
132 * If close to it, wait/fire as appropriate. Else, rotate.
133 *
134 * Occasionally, fire pod instead. Also occasionally, check for primed nodes
135 * and try to shoot them.
136 */
137
update(int time)138 void BasicAI::update(int time)
139 {
140 keys=0;
141
142 updateSeen();
143
144 const RelPolarCoord aim = gameState->you.aim;
145 const float aimAccuracy = gameState->you.aimAccuracy();
146 const float aimRate = gameState->youHaveNode(NODEC_PURPLE) ? 0.0006 : 0.0004;
147
148 HPInvader* inv = closestEnemy();
149
150 if ( !inv || dist(inv->cpos() - ARENA_CENTRE) > ARENA_RAD/3 )
151 {
152 // no immediate urgent threat - consider strategic aims
153
154 if (seed % 15 == 0 && gameState->you.podTimer <= 0)
155 {
156 // consider shooting a pod
157 if ( gameState->targettedNode != NULL &&
158 gameState->targettedNode->status != NODEST_YOU &&
159 (gameState->you.shootHeat <
160 gameState->you.shootMaxHeat - gameState->shotHeat(3)) )
161 {
162 keys |= K_POD;
163 newSeed();
164 }
165 else if (seed % 2)
166 keys |= K_LEFT;
167 else
168 keys |= K_RIGHT;
169 return;
170 }
171
172 if (seed % 4 == 0)
173 {
174 // consider shooting a primed node
175 Node* node = NULL;
176 Angle minangle;
177 for (std::vector<Node>::iterator it = gameState->nodes.begin();
178 it != gameState->nodes.end();
179 it++)
180 {
181 Angle dangle = it->pos.angle - gameState->you.aim.angle;
182 if (dangle > 2)
183 // normalise to [0,2]
184 dangle = -dangle;
185 if (it->status == NODEST_YOU && it->primed >= 1
186 && ( node == NULL || dangle < minangle ))
187 {
188 node = &*it;
189 minangle = dangle;
190 }
191 }
192 if (node)
193 inv=node;
194 }
195 }
196
197 if (inv)
198 {
199 // some tweakable parameters to affect behaviour:
200 // aimPerDist: 'aim.dist' to wait for is aimPerDist * (dist to target)
201 // aimRange: don't turn while this close (in sds) to target angle
202 // deAimRange: deaim to turn fast until this close to target angle
203 // estimateIterations: times through estimation loop
204 static const float aimPerDist = 0.3f;
205 static const float aimRange = 0.4f;
206 static const float deAimRange = 2.0f;
207 static const int estimateIterations = 3;
208
209 const int shotWeight = getShotWeight(inv->hp, inv->armour);
210 const bool super = gameState->youHaveNode(NodeColour(NODEC_GREEN + (shotWeight-1)));
211 const float shotSpeed = 0.1+0.05*(3-shotWeight) + super*0.05;
212
213 const RelPolarCoord invPos = inv->cpos() - ARENA_CENTRE;
214
215 // iteratively predict where we should want to turn to to give us
216 // sufficient time to aim and for the shot to get to the target.
217 // 'ppos' is current estimate of where the nasty will be when the shot
218 // hits it (so 'ppos.angle' is the angle to turn to before shooting);
219 // we start with 'ppos' being current position of target, and
220 // iteratively refine the estimate. Note that it will not tend to the
221 // correct answer, as we are using some linear estimates, e.g. in
222 // turnTime.
223 float targAimDist;
224 RelPolarCoord ppos = invPos;
225 for (int i=0; i<estimateIterations; i++)
226 {
227 targAimDist = std::min(AIM_MAX*2/3, aimPerDist*ppos.dist);
228 const float turnTime = fabs(angleDiff(aim.angle, ppos.angle)) /
229 (0.015*settings.turnRateFactor*aimAccuracy);
230 const float aimAtTarget = std::max(AIM_MIN, (float)(aim.dist-turnTime*.04));
231
232 // aiming time required: almost exact, the +1 is just to avoid
233 // division by 0.
234 const float aimTime =
235 -logf((AIM_MAX - targAimDist) / (AIM_MAX - aimAtTarget + 1)) / aimRate;
236
237 const float shootTime = ppos.dist / shotSpeed;
238
239 ppos = predictPos(inv, shootTime + aimTime);
240 }
241 const float relAimDist = std::max(0.0f, targAimDist - aim.dist);
242 const float relAngle = angleDiff(aim.angle, ppos.angle);
243 const float modRelAngle = fabs(angleDiff(aim.angle, ppos.angle));
244
245 // sameDir: our target angle is off from current angle in the same dir
246 // as our target is turning about us
247 const float invRelAngle = angleDiff(invPos.angle, ppos.angle);
248 const bool sameDir =
249 ((relAngle > 0) && (invRelAngle > 0)) ||
250 ((relAngle < 0) && (invRelAngle < 0));
251
252
253 if (modRelAngle <= aimRange*aimAccuracy &&
254 relAimDist == 0 &&
255 (gameState->you.shootHeat <
256 gameState->you.shootMaxHeat - gameState->shotHeat(shotWeight-1)
257 ))
258 {
259 keys |= K_SHOOT1 << (shotWeight-1);
260 newSeed();
261 }
262 else if (sameDir || modRelAngle > aimRange*aimAccuracy)
263 {
264 if (relAngle >= 0)
265 keys |= K_LEFT;
266 else
267 keys |= K_RIGHT;
268
269 if (modRelAngle > deAimRange*aimAccuracy)
270 keys |= K_DEAIM;
271 }
272 }
273 else
274 {
275 // haven't seen any valid enemies - spin around until we find one
276 keys |= K_DEAIM;
277 if (seed % 2)
278 keys |= K_LEFT;
279 else
280 keys |= K_RIGHT;
281 if (! rani(300))
282 newSeed();
283 }
284 }
285
286 // getShotWeight: returns, based on seed, a random number in [1,3] which
287 // is greater than armour and no more than hp+armour.
getShotWeight(int hp,int armour) const288 int BasicAI::getShotWeight(int hp, int armour) const
289 {
290 if (armour > 2)
291 // sanity check
292 return 1;
293
294 return armour + 1 + seed%std::min((3 - armour), hp);
295 }
296
297 // the seed determines current behaviour; we change it every time the current
298 // behaviour has been completed (e.g. we fire off a shot)
newSeed()299 void BasicAI::newSeed()
300 {
301 seed = rani(32767);
302 }
303
BasicAI(GameState * gameState)304 BasicAI::BasicAI(GameState* gameState) : AI(gameState)
305 {
306 newSeed();
307 }
308