1
################################################################################
2
# copyright 2008 Gabriel Pettier <gabriel.pettier@gmail.com>
4
# This file is part of UltimateSmashFriends
6
# UltimateSmashFriends is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
11
# UltimateSmashFriends is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with UltimateSmashFriends. If not, see <http://www.gnu.org/licenses/>.
18
# ##############################################################################
20
# Standard modules imports
24
# Custom modules imports
25
from entity import Entity
26
from config import config
28
# FIXME : Those global vars have to be set in the AI class, don't they ?
29
# This var defines the max distance, when two players are able to hit themselves
31
CRITICAL_DISTANCE = 25
32
# This var defines the distance within two players are considered as engaged
33
MIN_FIGHT_DISTANCE = 100
37
Provide a computer controlled player
38
Can be used like a normal entity (same methods)
41
def __init__ (self, num, game, entity_skinname = 'stick-tiny', skill = 'easy', place = (550, 1), lives = 3, carried_by = None, vector = (0, 0), reversed = False) :
43
Entity.__init__ (self, num, game, entity_skinname, place, lives, carried_by, vector, reversed)
45
self.enemy_position = []
46
self.enemy_distance = []
47
self.enemy_number = []
49
self.current_target = -1
51
self.target_x_offset = -1
52
self.target_y_offset = -1
53
self.target_x_relative_state = -1
54
self.target_y_relative_state = -1
57
self.fight_engaged = 0
58
self.need_change_target = 0
60
# TODO : Change that system, it's deprecated. Change the vars names too. And maybe those descriptions in a xml file ?
61
self.skill = skill # Utiliser un dico qui contiendra toutes les caracs ci-dessous
62
if self.skill == 'simplest' :
63
self.make_fall_priority = 0.00 # Ajouter un pourcentage d'action intelligentes realisees
64
elif self.skill == 'bad' :
65
self.make_fall_priority = 0.15
66
elif self.skill == 'easy' :
67
self.make_fall_priority = 0.30
68
elif self.skill == 'normal' :
69
self.make_fall_priority = 0.50
70
elif self.skill == 'good' :
71
self.make_fall_priority = 0.70
72
elif self.skill =='smart' :
73
self.make_fall_priority = 0.85
74
elif self.skill == 'extreme' :
75
self.make_fall_priority = 1.00
76
#self.min_limit_make_fall = 1.00 - self.make_fall_priority
79
Very important list, which will contain a list of actions to do, in the very near future
80
Structure : lifetime, remaining lifetime, action, options (player, etc... (in a list))
81
If there is no options, must be told with -1
82
If there is more than one action to do, they are listed in order, each one owning it own structure, like previously
83
Possible action : w = wait, k = kick (option : kind of kick, direction), m = walk (option : direction)
88
# If we need to ignore a player. If not use, TODO : remove
91
# This is used to know how much time use AI every turn
92
self.begin_computing_time = -1;
93
self.average_computing_time = [0, 0]
96
def update_enemy (self, game) :
98
This function update the information about different enemys
101
self.enemy_number = []
102
self.enemy_position = []
103
self.enemy_distance = []
105
for pl in [pl for pl in game.players if pl is not self] :
106
self.enemy_number.append (pl.num)
107
self.enemy_position.append (pl.place)
108
self.enemy_distance.append (self.dist (pl))
110
def get_enemy_num_by_rank (self, rank) :
112
This function is used when you choose a enemy with it's rank in a list
113
It will return you it's number
116
return self.enemy_number[rank]
118
def get_enemy_rank_by_num (self, num) :
120
This function is used when you got the number of an enemy,
121
and you need it's rank in lists
122
It will return you that
125
for rank, number in enumerate (self.enemy_number) :
129
def get_entity_by_num (self, num, game) :
131
This function simply returns an entity when you give it the entity
136
for pl in game.players :
139
# If not found, throw an exception ?
142
def reversed_or_not (self, side) :
144
This function defines if the AI player must be reversed or not,
145
depending on the side it must go, This is used to avoid some side test,
146
particularily in choose_strategy().
152
elif side == 'right' :
153
self.reversed = False
156
def kick (self, game, type = 'kick') :
158
This function is called by the others one when they decided to try to
159
hit a enemy. It could be used with any of the kick/combo types
162
# TODO: /!\ this should verify if the movement is allowed by sequences.cfg
163
# TODO : Decide who verify : self.kick or the function which call it ? (because if we can't make
164
# some kind of kick, the 'strategy' function must know it
165
# TODO : decide if we verify sequences.cfg or just implement another system thanks to strategy
166
self.entity_skin.change_animation (type, game, {'entity' : self})
169
def jump (self, game, type = 'simple') :
171
This function provide the different jumps
174
# TODO: /!\ this should verify if the movement is allowed by
176
if not self.in_jump and type == 'simple' :
177
self.entity_skin.change_animation ('jump', game, {'entity' : self})
180
elif self.in_jump <= 1 and type == 'double' :
181
self.entity_skin.change_animation ('scnd-jump', game, {'entity' : self})
186
# TODO : add 2 functions : one which allow to know if AI is doing something
187
# (i mean, like jumping or kick), and a second one, which allow
188
# to know if one particular kind of kick is possible
191
def compute_computing_time () :
193
This function just compute an average of the CPU time spent by AI
194
It uses a big aweful inline, but this one is useful in order
195
not to much time to loose
198
self.average_computing_time [0] = (self.average_computing_time [0] * self.average_computing_time [1]\
199
+ (time.clock () - self.begin_computing_time)) / (self.average_computing_time [1] + 1)
200
self.average_computing_time [1] += 1
203
def update_current_target (self, game) : # TODO
205
This function updates the current target
206
If the AI entity already got a target, it will probably keep the same, excepted if :
207
- An other function require a change
208
- An other target is really really near of the AI entity
210
If no target is actually defined :
211
In most case, it chooses the nearest target
212
It could also choose another one, if random.random () decide something else :)
214
And finally, if a enemy is really close, it will calculate some vars needed to choose a strategy
216
# TODO : when this function will work, it will need to be optimised (numerous call to get_*, etc...)
218
# We only want alive players, that are not us.
224
and player is not self\
227
# Case of a one-vs-one fight
228
if len(players_left) == 1 :
229
self.current_target = players_left[0].num
231
# TODO : write the else to choose the nearest enemy
232
# The following code must be in this "else"
236
# FIXME : errors in following code, i think
237
# If the closest enemy is really too close, or the current target too far,
238
# change current target to closest enemy
239
if closest[1] <= MIN_FIGHT_DISTANCE or closest[1] <= self.enemy_distance[self.get_enemy_rank_by_num (self.current_target.num)] :
240
self.current_target = closest[0]
243
if self.get_entity_by_num (self.current_target, game).dist (self) <= MIN_FIGHT_DISTANCE :
244
self.fight_engaged = True
245
elif self.get_entity_by_num (self.current_target, game).dist (self) >= MIN_FIGHT_DISTANCE :
246
self.fight_engaged = False
249
self.target_x_offset = self.place [0] - self.get_entity_by_num (self.current_target, game).place [0]
250
self.target_y_offset = self.place [1] - self.get_entity_by_num (self.current_target, game).place [1]
252
# If an enemy is critically close, set up new vars to choose a strategy
253
if self.fight_engaged :
254
if self.target_x_offset > CRITICAL_DISTANCE :
255
self.target_x_relative_state = 'left'
256
elif self.target_x_offset < - CRITICAL_DISTANCE :
257
self.target_x_relative_state = 'right'
259
self.target_x_relative_state = 'critical'
262
if self.target_y_offset > CRITICAL_DISTANCE :
263
self.target_y_relative_state = 'up'
264
elif self.target_y_offset < CRITICAL_DISTANCE :
265
self.target_y_relative_state = 'down'
267
self.target_y_relative_state = 'critical'
269
else : # The AI's not engaged, so empty the 'strategy' vars
270
self.target_x_relative_state = self.target_y_relative_state = ''
272
def test_on_ground (game, lenght = 5) :
273
return 'not implemented :P'
276
def jump_update (self, game) :
278
This function updates the in_jump var, for example if the entity just
279
landed or if it was walking and just fell It allows other AI function to
280
know it the entity is in the air, or on the ground
282
Possibles values for self.in_jump :
284
1 - flying, but can still make a big jump (second jump)
285
2 - flying and locked, it means the player already done the big jump
289
# FIXME : hack ! worldCollide () is too heavy for a single var !
290
self.worldCollide (game)
291
# Future form : self.test_on_ground (game, 5)
293
if self.onGround and self.in_jump :
296
if not self.onGround and not self.in_jump :
300
def localize_fall_zone (self, player, map) :
302
This function is called for a enemy the AI need to know where he can
307
test_len = 50 # FIXME : This is a constant which must be put somewhere else
309
# The two next rect are used to discover holes : the first one is higher
310
# than the ground, and must never collide (else, it means that there is
311
# an obstacle -- not a good way)
312
# The second, is 'in' the ground, and must always collide : else, we
314
test_rect_high = pygame.rect(
315
player.place [0] - test_len,
317
player.list_sin_cos [3][1]\
318
* player.entity_skin.animation.hardshape[3:4]\
320
+ player.entity_skin.animation.hardshape[3:4]\
327
test_rect_low = pygame.rect(
328
player.place [0] - test_len,
330
player.list_sin_cos [3][1]\
331
* player.entity_skin.animation.hardshape[3:4]\
333
+ player.entity_skin.animation.hardshape[3:4]\
341
distance = {'left' : -1, 'right' : -1}
345
if test_rect_high.colliderect (map) or test_rect_low [0] < 0:
347
elif not test_rect_low.colliderect (map) :
348
distance ['left'] = player.place [0] - (test_rect_low [0] + test_len)
351
test_rect_high [0] -= test_len
352
test_rect_low [0] -= test_len
354
# TODO : when a hole is detected, why not add a more accurate test ?
356
test_rect_high [0] = test_rect_low [0] = player.place [0]
358
# Right side test now
360
if test_rect_high.colliderect (map) or test_rect_low [0] > SIZE [0] :
362
elif not test_rect_low.colliderect (map) :
363
distance ['right'] = test_rect_low [0] - player.place [0]
366
test_rect_high [0] += test_len
367
test_rect_low [0] += test_len
369
# TODO : need something else now, or not ?
371
if distance ['left'] == distance ['right'] == -1 :
372
result = ('none', -1)
373
elif distance ['left'] == -1 :
374
result = ('right', distance ['right'])
375
elif distance ['right'] == -1 :
376
result = ('left', distance ['left'])
378
if distance ['left'] < distance ['right'] :
379
result = ('left', distance ['left'])
380
elif distance ['right'] < distance ['left'] :
381
result = ('right', distance ['right'])
383
result = ('both', distance ['left'])
388
def choose_strategy (self, game) : # TODO
390
This function defines self.strategy, and triggers action if necessairy
391
It means that it chooses the way which will be used to attack a enemy,
392
placed near of the AI entity It will choose :
395
- A place with a direction
397
# TODO : delete this one or the other one
398
self.update_current_target (game)
401
if self.in_jump : # TODO : We need to modify self.strategy here WTF ?
402
# Target is upper and AI is going down
404
# FIXME : regler prob des entity_skin.animation.vector
405
if self.target_y_relative_state == 'up'\
406
and self.entity_skin.animation.vector\
407
and self.entity_skin.animation.vector[1] >= 0:
408
if self.target_x_relative_state == 'critical':
409
self.kick (game, 'smash-up')
410
self.strategy = [0.1, 0.1, "w", -1] # TODO : the time (100) must be the duration of the kick
412
elif self.in_jump == 2 :
413
# AI is not completely under the target, and can't jump, so
415
self.walk (self.target_x_relative_state)
418
self.strategy = [0.1, 0.1, "m", self.target_x_relative_state] # TODO : is 50 too short, too long ?
421
self.reversed_or_not (self.target_x_relative_state)
424
# Same as above, and same TODO
425
self.strategy = [0.1, 0.1, "w", -1]
427
# Target is lower and AI is going up
428
if self.target_y_relative_state == 'down'\
429
and self.entity_skin.animation.vector\
430
and self.entity_skin.animation.vector[1] <= 0 :
431
if self.target_x_relative_state == 'critical' :
432
self.kick (game, 'smash-down')
434
# We are hiting, AI must wait
435
self.strategy = [0.1, 0.1, "w", -1] # TODO : Modidy 50 with the duration of a kick
439
self.walk (self.target_x_relative_state)
440
if self.target_y_relative_state == 'up'\
441
and self.entity_skin.animation.vector\
442
and self.entity_skin.animation.vector[1] <= 0\
443
or self.target_y_relative_state == 'down'\
444
and self.entity_skin.animation.vector\
445
and self.entity_skin.animation.vector [1] >= 0 :
448
self.strategy = [0.1, 0.1, "w", -1] # TODO : Duration ?
450
if self.target_y_relative_state == 'critical' :
451
if self.target_x_relative_state == 'critical' :
452
self.kick (game, 'smash-up')
454
self.strategy = [0.1, 0.1, "w", -1] # TODO : Duration of kick
457
self.reversed_or_not (self.target_x_relative_state)
458
self.kick (game, 'smash-straight')
460
self.strategy = [0.1, 0.1, "w", -1] # TODO : duration
462
elif self.fight_engaged :
463
if self.target_y_relative_state == 'critical' :
464
if self.target_x_relative_state is not 'critical' :
465
self.reversed_or_not (self.target_x_relative_state)
466
self.kick (game, 'kick-jumping')
468
self.strategy = [0.1, 0.1, "w", -1]
471
if self.target_y_relative_state == 'up' :
472
if self.target_x_relative_state is 'critical' :
473
self.kick (game, 'smash-up')
475
self.strategy = [0.1, 0.1, "w", -1]
479
def use_strategy (self, game, dt) : # TODO
481
This function is the main AI function.
482
It chooses what the AI entity will do the next frames
484
- Find a long path to the target (which is far away)
485
- Just move simply to the target, if it is near and on the same level
486
- Choose a strategy (what to kick, when, in what direction) if a enemy
490
# TODO : delete this one or the other one
491
self.update_current_target (game)
493
# Okay, strategy already set, verify (really ?) and apply it
495
if self.strategy [2] == "m" :
496
self.walk (self.strategy [3])
498
elif self.strategy [2] == "k" :
499
self.reversed_or_not (self.strategy [3][1])
500
self.kick (game, self.strategy [3][0])
503
# Now, let decrease remaing lifetime of this strategy, and delete it if decaprecated
504
self.strategy [1] -= dt
505
if self.strategy [1] <= 0 :
506
# Delete only 4 element, in case there is more than one action to do
507
del self.strategy [0 : 4]
510
# No strategy, lets find one or simply move to the target
512
if self.fight_engaged :
513
self.choose_strategy (game)
516
self.find_path (game)
518
print self.strategy, self.fight_engaged, self.target_x_relative_state, self.target_y_relative_state
521
def walk (self, side = 'left' ) :
523
This function, of course, allows the AI entity to walk and aims to
524
reproduce the human player move
528
self.walking_vector [0] = WALKSPEED
531
elif side == 'right' :
532
self.walking_vector [0] = WALKSPEED
533
self.reversed = False
535
elif side == 'stop' :
536
self.walking_vector [0] = 0
539
def find_path (self, game) : # TODO
541
This function is used when a target is far away from the AI entity
542
It will find a way to the target
546
# All that is only debug, it's not a real pathfinder
547
self.target_x_offset = self.place [0] - self.get_entity_by_num (self.current_target, game).place [0]
549
if self.target_x_offset > 0 :
554
if (self.target_x_offset ** 2)** 0.5 <= 5 :
558
def update (self, dt, t, surface, game, coords = (0 ,0), zoom = 1) : # TODO
560
This function is the function called each game loop It call all the
561
functions needed to update the AI, and to allow it to
566
begin_computing_time = time.clock ()
568
# Get the position of other players (potential target), and the distance to them
569
self.update_enemy (game)
571
# Update the jump status
572
self.jump_update (game)
574
# Choose action to do
575
self.use_strategy (game, dt)
577
# Apply pending moves and gravity
578
self.update_physics (dt, game)
580
# Update animation of entity
581
if self.entity_skin.update (t, self.reversed) == 0 :
584
end_computing_time = time.clock ();