~alaxa27/ultimate-smash-friends/mirror_trunk

« back to all changes in this revision

Viewing changes to pkg/ultimate-smash-friends_1.0-beta-1/usr/lib/usf_modules/AI.p

  • Committer: gaby
  • Date: 2009-11-30 17:03:16 UTC
  • Revision ID: gaby@ks22672.kimsufi.com-20091130170316-6lm3v7q0torulfab
adding code package

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
################################################################################
 
2
# copyright 2008 Gabriel Pettier <gabriel.pettier@gmail.com>
 
3
#
 
4
# This file is part of UltimateSmashFriends
 
5
#
 
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.
 
10
#
 
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.
 
15
#
 
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
# ##############################################################################
 
19
 
 
20
# Standard modules imports
 
21
import random
 
22
import time
 
23
 
 
24
# Custom modules imports
 
25
from entity import Entity
 
26
from config import config
 
27
 
 
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
 
30
# without moving
 
31
CRITICAL_DISTANCE = 25
 
32
# This var defines the distance within two players are considered as engaged
 
33
MIN_FIGHT_DISTANCE = 100
 
34
 
 
35
class AI (Entity):
 
36
    """
 
37
    Provide a computer controlled player
 
38
    Can be used like a normal entity (same methods)
 
39
    """
 
40
 
 
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) :
 
42
 
 
43
        Entity.__init__ (self, num, game, entity_skinname, place, lives, carried_by, vector, reversed)
 
44
 
 
45
        self.enemy_position = []
 
46
        self.enemy_distance = []
 
47
        self.enemy_number = []
 
48
 
 
49
        self.current_target = -1
 
50
 
 
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
 
55
 
 
56
        self.in_jump = 0
 
57
        self.fight_engaged = 0
 
58
        self.need_change_target = 0
 
59
 
 
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
 
77
 
 
78
        """
 
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)
 
84
 
 
85
        """
 
86
        self.strategy = []
 
87
 
 
88
        # If we need to ignore a player. If not use, TODO : remove
 
89
        self.ignored_pl = []
 
90
        
 
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]
 
94
 
 
95
 
 
96
    def update_enemy (self, game) :
 
97
        """
 
98
        This function update the information about different enemys
 
99
        """
 
100
 
 
101
        self.enemy_number = []
 
102
        self.enemy_position = []
 
103
        self.enemy_distance = []
 
104
 
 
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))
 
109
 
 
110
    def get_enemy_num_by_rank (self, rank) :
 
111
        """
 
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
 
114
        """
 
115
 
 
116
        return self.enemy_number[rank]
 
117
 
 
118
    def get_enemy_rank_by_num (self, num) :
 
119
        """
 
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
 
123
        """
 
124
 
 
125
        for rank, number in enumerate (self.enemy_number) :
 
126
            if number == num :
 
127
                return rank
 
128
 
 
129
    def get_entity_by_num (self, num, game) :
 
130
        """
 
131
        This function simply returns an entity when you give it the entity
 
132
        number
 
133
 
 
134
        """
 
135
 
 
136
        for pl in game.players :
 
137
            if pl.num == num :
 
138
                return pl
 
139
        # If not found, throw an exception ?
 
140
 
 
141
 
 
142
    def reversed_or_not (self, side) :
 
143
        """
 
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().
 
147
 
 
148
        """
 
149
 
 
150
        if side == 'left' :
 
151
            self.reversed = True
 
152
        elif side == 'right' :
 
153
            self.reversed = False
 
154
 
 
155
 
 
156
    def kick (self, game, type = 'kick') :
 
157
        """
 
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
 
160
        """
 
161
 
 
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})
 
167
 
 
168
 
 
169
    def jump (self, game, type = 'simple') :
 
170
        """
 
171
        This function provide the different jumps
 
172
        """
 
173
 
 
174
        # TODO: /!\ this should verify if the movement is allowed by
 
175
        # sequences.cfg
 
176
        if not self.in_jump and type == 'simple' :
 
177
            self.entity_skin.change_animation ('jump', game, {'entity' : self})
 
178
            self.in_jump = 1
 
179
 
 
180
        elif self.in_jump <= 1 and type == 'double' :
 
181
            self.entity_skin.change_animation ('scnd-jump', game, {'entity' : self})
 
182
            self.in_jump = 2
 
183
 
 
184
 
 
185
 
 
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
 
189
    
 
190
    
 
191
    def compute_computing_time () :
 
192
        """
 
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
 
196
        """
 
197
        
 
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 
 
201
 
 
202
 
 
203
    def update_current_target (self, game) : # TODO
 
204
        """
 
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
 
209
 
 
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 :)
 
213
 
 
214
        And finally, if a enemy is really close, it will calculate some vars needed to choose a strategy
 
215
        """
 
216
        # TODO : when this function will work, it will need to be optimised (numerous call to get_*, etc...)
 
217
 
 
218
        # We only want alive players, that are not us.
 
219
        players_left = [
 
220
                        player\
 
221
                        for player\
 
222
                        in game.players\
 
223
                        if player.lives > 0\
 
224
                        and player is not self\
 
225
                       ]
 
226
 
 
227
        # Case of a one-vs-one fight
 
228
        if len(players_left) == 1 :
 
229
            self.current_target = players_left[0].num
 
230
 
 
231
        # TODO : write the else to choose the nearest enemy
 
232
        # The following code must be in this "else"
 
233
        
 
234
        """
 
235
 
 
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]
 
241
        """
 
242
 
 
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
 
247
        
 
248
        
 
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]
 
251
 
 
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'
 
258
            else :
 
259
                self.target_x_relative_state = 'critical'
 
260
                self.walk ("stop")
 
261
 
 
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'
 
266
            else :
 
267
                self.target_y_relative_state = 'critical'
 
268
 
 
269
        else : # The AI's not engaged, so empty the 'strategy' vars
 
270
            self.target_x_relative_state = self.target_y_relative_state = ''
 
271
 
 
272
    def test_on_ground (game, lenght = 5) :
 
273
        return 'not implemented :P'
 
274
 
 
275
 
 
276
    def jump_update (self, game) :
 
277
        """
 
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
 
281
 
 
282
        Possibles values for self.in_jump :
 
283
        0 - on ground
 
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
 
286
 
 
287
        """
 
288
 
 
289
        # FIXME : hack ! worldCollide () is too heavy for a single var !
 
290
        self.worldCollide (game)
 
291
        # Future form : self.test_on_ground (game, 5)
 
292
 
 
293
        if self.onGround and self.in_jump :
 
294
            self.in_jump = 0
 
295
 
 
296
        if not self.onGround and not self.in_jump :
 
297
            self.in_jump = 1
 
298
 
 
299
 
 
300
    def localize_fall_zone (self, player, map) :
 
301
        """
 
302
        This function is called for a enemy the AI need to know where he can
 
303
        fall
 
304
 
 
305
        """
 
306
 
 
307
        test_len = 50 # FIXME : This is a constant which must be put somewhere else
 
308
 
 
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
 
313
        # found an hole \o/
 
314
        test_rect_high = pygame.rect(
 
315
                                        player.place [0] - test_len,
 
316
                                        (
 
317
                                            player.list_sin_cos [3][1]\
 
318
                                            * player.entity_skin.animation.hardshape[3:4]\
 
319
                                            / 2\
 
320
                                            + player.entity_skin.animation.hardshape[3:4]\
 
321
                                            / 2
 
322
                                            + player.place[1]
 
323
                                        ),
 
324
                                        test_len,
 
325
                                        1
 
326
                                    )
 
327
        test_rect_low = pygame.rect(
 
328
                                        player.place [0] - test_len,
 
329
                                        (
 
330
                                            player.list_sin_cos [3][1]\
 
331
                                            * player.entity_skin.animation.hardshape[3:4]\
 
332
                                                / 2
 
333
                                            + player.entity_skin.animation.hardshape[3:4]\
 
334
                                                / 2\
 
335
                                            + player.place [1]
 
336
                                        ) - 1,
 
337
                                        test_len,
 
338
                                        1
 
339
                                   )
 
340
 
 
341
        distance = {'left' : -1, 'right' : -1}
 
342
 
 
343
        # Left side test
 
344
        while true :
 
345
            if test_rect_high.colliderect (map) or test_rect_low [0] < 0:
 
346
                break
 
347
            elif not test_rect_low.colliderect (map) :
 
348
                distance ['left'] = player.place [0] - (test_rect_low [0] + test_len)
 
349
                break
 
350
            else :
 
351
                test_rect_high [0] -= test_len
 
352
                test_rect_low [0] -= test_len
 
353
 
 
354
        # TODO : when a hole is detected, why not add a more accurate test ?
 
355
 
 
356
        test_rect_high [0] = test_rect_low [0] = player.place [0]
 
357
 
 
358
        # Right side test now
 
359
        while true :
 
360
            if test_rect_high.colliderect (map) or test_rect_low [0] > SIZE [0] :
 
361
                break
 
362
            elif not test_rect_low.colliderect (map) :
 
363
                distance ['right'] = test_rect_low [0] - player.place [0]
 
364
                brek
 
365
            else :
 
366
                test_rect_high [0] += test_len
 
367
                test_rect_low [0] += test_len
 
368
 
 
369
        # TODO : need something else now, or not ?
 
370
 
 
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'])
 
377
        else :
 
378
            if distance ['left'] < distance ['right'] :
 
379
                result = ('left', distance ['left'])
 
380
            elif distance ['right'] < distance ['left'] :
 
381
                result = ('right', distance ['right'])
 
382
            else :
 
383
                result = ('both', distance ['left'])
 
384
 
 
385
        return result
 
386
 
 
387
 
 
388
    def choose_strategy (self, game) : # TODO
 
389
        """
 
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 :
 
393
        - A kick/combo
 
394
        - A time
 
395
        - A place with a direction
 
396
        """
 
397
        # TODO : delete this one or the other one
 
398
        self.update_current_target (game)
 
399
        
 
400
       
 
401
        if self.in_jump : # TODO : We need to modify self.strategy here            WTF ?
 
402
            # Target is upper and AI is going down
 
403
 
 
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
 
411
 
 
412
                elif self.in_jump == 2 :
 
413
                    # AI is not completely under the target, and can't jump, so
 
414
                    # move to the entity
 
415
                    self.walk (self.target_x_relative_state)
 
416
 
 
417
                    # Wait and update
 
418
                    self.strategy = [0.1, 0.1, "m", self.target_x_relative_state] # TODO : is 50 too short, too long ?
 
419
 
 
420
                else :
 
421
                    self.reversed_or_not (self.target_x_relative_state)
 
422
                    self.jump ('double')
 
423
 
 
424
                    # Same as above, and same TODO
 
425
                    self.strategy = [0.1, 0.1, "w", -1]
 
426
 
 
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')
 
433
 
 
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
 
436
 
 
437
 
 
438
                else :
 
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 :
 
446
 
 
447
 
 
448
                self.strategy = [0.1, 0.1, "w", -1] # TODO : Duration ?
 
449
 
 
450
            if self.target_y_relative_state == 'critical' :
 
451
                if self.target_x_relative_state == 'critical' :
 
452
                    self.kick (game, 'smash-up')
 
453
 
 
454
                    self.strategy = [0.1, 0.1, "w", -1] # TODO : Duration of kick
 
455
 
 
456
                else :
 
457
                    self.reversed_or_not (self.target_x_relative_state)
 
458
                    self.kick (game, 'smash-straight')
 
459
 
 
460
                    self.strategy = [0.1, 0.1, "w", -1] # TODO : duration
 
461
 
 
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')
 
467
                    
 
468
                    self.strategy = [0.1, 0.1, "w", -1]
 
469
                    
 
470
                    
 
471
            if self.target_y_relative_state == 'up' :
 
472
                if self.target_x_relative_state is 'critical' :
 
473
                    self.kick (game, 'smash-up')
 
474
                    
 
475
                    self.strategy = [0.1, 0.1, "w", -1]
 
476
        
 
477
        
 
478
 
 
479
    def use_strategy (self, game, dt) : # TODO
 
480
        """
 
481
        This function is the main AI function.
 
482
        It chooses what the AI entity will do the next frames
 
483
        It could choose to :
 
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
 
487
          is near enought
 
488
        """
 
489
 
 
490
        # TODO : delete this one or the other one
 
491
        self.update_current_target (game)
 
492
        
 
493
        # Okay, strategy already set, verify (really ?) and apply it
 
494
        if self.strategy :
 
495
            if self.strategy [2] == "m" :
 
496
                self.walk (self.strategy [3])
 
497
 
 
498
            elif self.strategy [2] == "k" :
 
499
                self.reversed_or_not (self.strategy [3][1])
 
500
                self.kick (game, self.strategy [3][0])
 
501
                
 
502
 
 
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]
 
508
                
 
509
 
 
510
        # No strategy, lets find one or simply move to the target
 
511
        else :
 
512
            if self.fight_engaged :
 
513
                self.choose_strategy (game)
 
514
 
 
515
            else :
 
516
                self.find_path (game)
 
517
                
 
518
        print self.strategy, self.fight_engaged, self.target_x_relative_state, self.target_y_relative_state
 
519
 
 
520
 
 
521
    def walk (self, side = 'left' ) :
 
522
        """
 
523
        This function, of course, allows the AI entity to walk and aims to
 
524
        reproduce the human player move
 
525
 
 
526
        """
 
527
        if side == 'left' :
 
528
            self.walking_vector [0] = WALKSPEED
 
529
            self.reversed = True
 
530
 
 
531
        elif side == 'right' :
 
532
            self.walking_vector [0] = WALKSPEED
 
533
            self.reversed = False
 
534
 
 
535
        elif side == 'stop' :
 
536
            self.walking_vector [0] = 0
 
537
 
 
538
 
 
539
    def find_path (self, game) : # TODO
 
540
        """
 
541
        This function is used when a target is far away from the AI entity
 
542
        It will find a way to the target
 
543
 
 
544
        """
 
545
        
 
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]
 
548
        
 
549
        if self.target_x_offset > 0 :
 
550
            self.walk ("left")
 
551
        else :
 
552
            self.walk ("right")
 
553
 
 
554
        if (self.target_x_offset ** 2)** 0.5 <= 5 :
 
555
            self.walk ('stop')
 
556
 
 
557
 
 
558
    def update (self, dt, t, surface, game, coords = (0 ,0), zoom = 1) : # TODO
 
559
        """
 
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
 
562
        find actions to do
 
563
 
 
564
        """
 
565
        
 
566
        begin_computing_time = time.clock ()
 
567
 
 
568
        # Get the position of other players (potential target), and the distance to them
 
569
        self.update_enemy (game)
 
570
 
 
571
        # Update the jump status
 
572
        self.jump_update (game)
 
573
 
 
574
        # Choose action to do
 
575
        self.use_strategy (game, dt)
 
576
 
 
577
        # Apply pending moves and gravity
 
578
        self.update_physics (dt, game)
 
579
 
 
580
        # Update animation of entity
 
581
        if self.entity_skin.update (t, self.reversed) == 0 :
 
582
            del (self)
 
583
            
 
584
        end_computing_time = time.clock ();