source: gamelib/animal.py @ 146:423050ec188b

Last change on this file since 146:423050ec188b was 146:423050ec188b, checked in by Jeremy Thurgood <firxen@…>, 11 years ago

Equipment images.

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