source: gamelib/animal.py @ 145:490ede177f50

Last change on this file since 145:490ede177f50 was 145:490ede177f50, checked in by Neil Muller <drnlmuller@…>, 11 years ago

Tweak fox behaviour around henhouses. Add some memory to avoid the indecisive fox loop

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