source: gamelib/animal.py @ 199:696936621a93

Last change on this file since 199:696936621a93 was 199:696936621a93, checked in by Jeremy Thurgood <firxen@…>, 11 years ago

Buildings can affect visual acuity.

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