source: gamelib/animal.py @ 200:67d10f7e0159

Last change on this file since 200:67d10f7e0159 was 200:67d10f7e0159, checked in by Adrianna Pińska <adrianna.pinska@…>, 11 years ago

selected chickens are selected

File size: 15.2 KB
Line 
1"""Class for the various animals in the game"""
2
3import random
4
5from pgu.vid import Sprite
6from pgu.algo import getline
7
8import imagecache
9import tiles
10from misc import Position
11import sound
12import equipment
13
14class Animal(Sprite):
15    """Base class for animals"""
16
17    STEALTH = 0
18    VISION_BONUS = 0
19    VISION_RANGE_PENALTY = 10
20
21    def __init__(self, image_left, image_right, tile_pos):
22        # Create the animal somewhere far off screen
23        Sprite.__init__(self, image_left, (-1000, -1000))
24        self._image_left = image_left
25        self.image_left = image_left.copy()
26        self._image_right = image_right
27        self.image_right = image_right.copy()
28        if hasattr(tile_pos, 'to_tuple'):
29            self.pos = tile_pos
30        else:
31            self.pos = Position(tile_pos[0], tile_pos[1])
32        self.equipment = []
33        self.accoutrements = []
34        self.abode = None
35        self.facing = 'left'
36
37    def loop(self, tv, _sprite):
38        ppos = tv.tile_to_view(self.pos.to_tuple())
39        self.rect.x = ppos[0]
40        self.rect.y = ppos[1]
41
42    def move(self, state):
43        """Given the game state, return a new position for the object"""
44        # Default is not to move
45        pass
46
47    def set_pos(self, tile_pos):
48        """Move an animal to the given tile_pos."""
49        new_pos = Position(*tile_pos)
50        self._fix_face(new_pos)
51        self.pos = new_pos
52
53    def _fix_face(self, final_pos):
54        """Set the face correctly"""
55        if final_pos.left_of(self.pos):
56            self._set_image_facing('left')
57        elif final_pos.right_of(self.pos):
58            self._set_image_facing('right')
59
60    def _set_image_facing(self, facing):
61        self.facing = facing
62        if self.facing == 'left':
63            self.setimage(self.image_left)
64        elif self.facing == 'right':
65            self.setimage(self.image_right)
66
67    def equip(self, item):
68        if equipment.is_equipment(item):
69            self.equipment.append(item)
70        elif equipment.is_accoutrement(item):
71            self.accoutrements.append(item)
72        self.redraw()
73
74    def unequip(self, item):
75        if equipment.is_equipment(item):
76            self.equipment = [e for e in self.equipment if e != item]
77        elif equipment.is_accoutrement(item):
78            self.accoutrements = [e for e in self.accoutrements if e != item]
79        self.redraw()
80
81    def unequip_by_name(self, item_name):
82        # only remove first match
83        matches = [item for item in self.equipment + self.accoutrements if item.NAME == item_name]
84        if matches:
85            self.unequip(matches[0])
86
87    def redraw(self):
88        layers = [(self._image_left.copy(), self._image_right.copy(), 0)]
89        if hasattr(self, 'EQUIPMENT_IMAGE_ATTRIBUTE'):
90            for item in self.accoutrements + self.equipment:
91                images = item.images(self.EQUIPMENT_IMAGE_ATTRIBUTE)
92                if images:
93                    layers.append(images)
94
95        layers.sort(key=lambda l: l[2])
96
97        self.image_left = layers[0][0]
98        self.image_right = layers[0][1]
99        for l in layers[1:]:
100            self.image_left.blit(l[0], (0,0))
101            self.image_right.blit(l[1], (0,0))
102
103        self._set_image_facing(self.facing)
104
105    def weapons(self):
106        return [e for e in self.equipment if equipment.is_weapon(e)]
107
108    def armour(self):
109        return [e for e in self.equipment if equipment.is_armour(e)]
110
111    def covers(self, tile_pos):
112        return tile_pos[0] == self.pos.x and tile_pos[1] == self.pos.y
113
114    def outside(self):
115        return self.abode is None
116
117    def survive_damage(self):
118        for a in self.armour():
119            if not a.survive_damage():
120                self.unequip(a)
121            return True
122        return False
123
124class Chicken(Animal):
125    """A chicken"""
126
127    EQUIPMENT_IMAGE_ATTRIBUTE = 'CHICKEN_IMAGE_FILE'
128
129    def __init__(self, pos):
130        image_left = imagecache.load_image('sprites/chkn.png')
131        image_right = imagecache.load_image('sprites/chkn.png',
132                ("right_facing",))
133        Animal.__init__(self, image_left, image_right, pos)
134        self.egg = None
135        self.egg_counter = 0
136
137    def move(self, gameboard):
138        """A free chicken will move away from other free chickens"""
139        pass
140
141    def lay(self):
142        """See if the chicken lays an egg"""
143        if not self.egg:
144            self.egg = Egg(self.pos)
145
146    def hatch(self):
147        """See if we have an egg to hatch"""
148        if self.egg:
149            chick = self.egg.hatch()
150            if chick:
151                self.egg = None
152            return chick
153        return None
154
155    def _find_killable_fox(self, weapon, gameboard):
156        """Choose a random fox within range of this weapon."""
157        killable_foxes = []
158        for fox in gameboard.foxes:
159            if not visible(self, fox):
160                continue
161            if weapon.in_range(gameboard, self, fox):
162                killable_foxes.append(fox)
163        if not killable_foxes:
164            return None
165        return random.choice(killable_foxes)
166
167    def attack(self, gameboard):
168        """An armed chicken will attack a fox within range."""
169        if not self.weapons():
170            # Not going to take on a fox bare-winged.
171            return
172        # Choose the first weapon equipped.
173        weapon = self.weapons()[0]
174        fox = self._find_killable_fox(weapon, gameboard)
175        if not fox:
176            return
177        if weapon.hit(gameboard, self, fox):
178            sound.play_sound("kill-fox.ogg")
179            gameboard.kill_fox(fox)
180
181class Egg(Animal):
182    """An egg"""
183
184    def __init__(self, pos):
185        image = imagecache.load_image('sprites/egg.png')
186        Animal.__init__(self, image, image, pos)
187        self.counter = 2
188
189    # Eggs don't move
190
191    def hatch(self):
192        self.counter -= 1
193        if self.counter == 0:
194            return Chicken(self.pos)
195        return None
196
197class Fox(Animal):
198    """A fox"""
199
200    STEALTH = 20
201    IMAGE_FILE = 'sprites/fox.png'
202
203    costs = {
204            # weighting for movement calculation
205            'grassland' : 2,
206            'woodland' : 1, # Try to keep to the woods if possible
207            'broken fence' : 2,
208            'fence' : 10,
209            'guardtower' : 2, # We can pass under towers
210            'henhouse' : 30, # Don't go into a henhouse unless we're going to
211                             # catch a chicken there
212            'hendominium' : 30,
213            }
214
215    def __init__(self, pos):
216        image_left = imagecache.load_image(self.IMAGE_FILE)
217        image_right = imagecache.load_image(self.IMAGE_FILE, ("right_facing",))
218        Animal.__init__(self, image_left, image_right, pos)
219        self.landmarks = [self.pos]
220        self.hunting = True
221        self.dig_pos = None
222        self.tick = 0
223        self.safe = False
224        self.closest = None
225        self.last_steps = []
226
227    def _cost_tile(self, pos, gameboard):
228        if gameboard.in_bounds(pos):
229            this_tile = gameboard.tv.get(pos.to_tuple())
230            cost = self.costs.get(tiles.TILE_MAP[this_tile], 100)
231        else:
232            cost = 100 # Out of bounds is expensive
233        return cost
234
235    def _cost_path(self, path, gameboard):
236        """Calculate the cost of a path"""
237        total = 0
238        for pos in path:
239            total += self._cost_tile(pos, gameboard)
240        return total
241
242    def _gen_path(self, start_pos, final_pos):
243        """Construct a direct path from start_pos to final_pos,
244           excluding start_pos"""
245        if abs(start_pos.x - final_pos.x) < 2 and \
246                abs(start_pos.y - final_pos.y) < 2:
247            # pgu gets this case wrong on occasion.
248            return [final_pos]
249        start = start_pos.to_tuple()
250        end = final_pos.to_tuple()
251        points = getline(start, end)
252        points.remove(start) # exclude start_pos
253        if end not in points:
254            # Rounding errors in getline cause this
255            points.append(end)
256        return [Position(x[0], x[1]) for x in points]
257
258    def _find_best_path_step(self, final_pos, gameboard):
259        """Find the cheapest path to final_pos, and return the next step
260           along the path."""
261        # We calculate the cost of the direct path
262        direct_path = self._gen_path(self.pos, final_pos)
263        min_cost = self._cost_path(direct_path, gameboard)
264        min_path = direct_path
265        # is there a point nearby that gives us a cheaper direct path?
266        # This is delibrately not finding the optimal path, as I don't
267        # want the foxes to be too intelligent, although the implementation
268        # isn't well optimised yet
269        poss = [Position(x, y) for x in range(self.pos.x - 3, self.pos.x + 4)
270                for y in range(self.pos.y - 3, self.pos.y + 4)
271                if (x, y) != (0,0)]
272        for start in poss:
273            cand_path = self._gen_path(self.pos, start) + \
274                    self._gen_path(start, final_pos)
275            cost = self._cost_path(cand_path, gameboard)
276            if cost < min_cost:
277                min_cost = cost
278                min_path = cand_path
279        if not min_path:
280            return final_pos
281        return min_path[0]
282
283    def _find_path_to_woodland(self, gameboard):
284        """Dive back to woodland through the landmarks"""
285        # find the closest point to our current location in walked path
286        if self.pos == self.landmarks[-1]:
287            if len(self.landmarks) > 1:
288                self.landmarks.pop() # Moving to the next landmark
289            else:
290                # Safely back at the start
291                self.safe = True
292                return self.pos
293        return self._find_best_path_step(self.landmarks[-1], gameboard)
294
295    def _find_path_to_chicken(self, gameboard):
296        """Find the path to the closest chicken"""
297        # Find the closest chicken
298        min_dist = 999
299        if self.closest not in gameboard.chickens:
300            # Either no target, or someone ate it
301            self.closest = None
302            for chicken in gameboard.chickens:
303                dist = chicken.pos.dist(self.pos)
304                if chicken.abode:
305                    dist += 10 # Prefer free-ranging chickens
306                if dist < min_dist:
307                    min_dist = dist
308                    self.closest = chicken
309        if not self.closest:
310            # No more chickens, so leave
311            self.hunting = False
312            return self.pos
313        if self.closest.pos == self.pos:
314            # Caught a chicken
315            self._catch_chicken(self.closest, gameboard)
316            return self.pos
317        return self._find_best_path_step(self.closest.pos, gameboard)
318
319    def _catch_chicken(self, chicken, gameboard):
320        """Catch a chicken"""
321        if not chicken.survive_damage():
322            sound.play_sound("kill-chicken.ogg")
323            gameboard.remove_chicken(chicken)
324        self.closest = None
325        self.hunting = False
326        self.last_steps = [] # Forget history here
327
328    def _update_pos(self, gameboard, new_pos):
329        """Update the position, making sure we don't step on other foxes"""
330        if new_pos == self.pos:
331            # We're not moving, so we can skip all the checks
332            return new_pos
333        final_pos = new_pos
334        blocked = final_pos in self.last_steps
335        moves = [Position(x, y) for x in range(self.pos.x-1, self.pos.x + 2)
336                for y in range(self.pos.y-1, self.pos.y + 2)
337                if Position(x,y) != self.pos and \
338                        Position(x, y) not in self.last_steps]
339        for fox in gameboard.foxes:
340            if fox is not self and fox.pos == final_pos:
341                blocked = True
342            if fox.pos in moves:
343                moves.remove(fox.pos)
344        if blocked:
345            # find the cheapest point in moves to new_pos that's not blocked
346            final_pos = None
347            min_cost = 1000
348            for poss in moves:
349                cost = self._cost_tile(poss, gameboard)
350                if cost < min_cost:
351                    min_cost = cost
352                    final_pos = poss
353        if not final_pos:
354            # No good choice, so stay put
355            return self.pos
356        if gameboard.in_bounds(final_pos):
357            this_tile = gameboard.tv.get(final_pos.to_tuple())
358        else:
359            this_tile = tiles.REVERSE_TILE_MAP['woodland']
360        if tiles.TILE_MAP[this_tile] == 'broken fence' and self.hunting:
361            # We'll head back towards the holes we make/find
362            self.landmarks.append(final_pos)
363        elif tiles.TILE_MAP[this_tile] == 'fence' and not self.dig_pos:
364            self._dig(final_pos)
365            return self.pos
366        self.last_steps.append(final_pos)
367        if len(self.last_steps) > 3:
368            self.last_steps.pop(0)
369        return final_pos
370
371    def _dig(self, dig_pos):
372        """Setup dig parameters, to be overridden if needed"""
373        self.tick = 5
374        self.dig_pos = dig_pos
375
376    def _make_hole(self, gameboard):
377        """Make a hole in the fence"""
378        gameboard.tv.set(self.dig_pos.to_tuple(),
379                tiles.REVERSE_TILE_MAP['broken fence'])
380        self.dig_pos = None
381
382    def move(self, gameboard):
383        """Foxes will aim to move towards the closest henhouse or free
384           chicken"""
385        if self.dig_pos:
386            if self.tick:
387                self.tick -= 1
388                # We're still digging through the fence
389                # Check the another fox hasn't dug a hole for us
390                # We're too busy digging to notice if a hole appears nearby,
391                # but we'll notice if the fence we're digging vanishes
392                this_tile = gameboard.tv.get(self.dig_pos.to_tuple())
393                if tiles.TILE_MAP[this_tile] == 'broken fence':
394                    self.tick = 0 
395                return
396            else:
397                # We've dug through the fence, so make a hole
398                self._make_hole(gameboard)
399            return 
400        if self.hunting:
401            desired_pos = self._find_path_to_chicken(gameboard)
402        else:
403            desired_pos = self._find_path_to_woodland(gameboard)
404        final_pos = self._update_pos(gameboard, desired_pos)
405        self._fix_face(final_pos)
406        self.pos = final_pos
407
408class NinjaFox(Fox):
409    """Ninja foxes are hard to see"""
410
411    STEALTH = 60
412    IMAGE_FILE = 'sprites/ninja_fox.png'
413
414class DemoFox(Fox):
415    """Demolition Foxes destroy fences easily"""
416
417class GreedyFox(Fox):
418    """Greedy foxes eat more chickens"""
419
420    def __init__(self, pos):
421        Fox.__init__(self, pos)
422        self.chickens_eaten = 0
423
424    def _catch_chicken(self, chicken, gameboard):
425        gameboard.remove_chicken(chicken)
426        self.closest = None
427        self.chickens_eaten += 1
428        if self.chickens_eaten > 2:
429            self.hunting = False
430        self.last_steps = []
431
432def _get_vision_param(parameter, watcher):
433    param = getattr(watcher, parameter)
434    if watcher.abode:
435        modifier = getattr(watcher.abode.building, 'MODIFY_'+parameter, lambda r: r)
436        param = modifier(param)
437    return param
438
439def visible(watcher, watchee):
440    vision_bonus = _get_vision_param('VISION_BONUS', watcher)
441    range_penalty = _get_vision_param('VISION_RANGE_PENALTY', watcher)
442    distance = watcher.pos.dist(watchee.pos) - 1
443    roll = random.randint(1, 100)
444    return roll > watchee.STEALTH - vision_bonus + range_penalty*distance
Note: See TracBrowser for help on using the repository browser.