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