changeset 70:d92a2f973cc4

Make foxes move 'better' and break fences
author Neil Muller <drnlmuller@gmail.com>
date Mon, 31 Aug 2009 22:14:41 +0000
parents 18db99fda6bd
children 00cf9d7f22dc
files gamelib/animal.py
diffstat 1 files changed, 171 insertions(+), 46 deletions(-) [+]
line wrap: on
line diff
--- a/gamelib/animal.py	Mon Aug 31 22:14:12 2009 +0000
+++ b/gamelib/animal.py	Mon Aug 31 22:14:41 2009 +0000
@@ -1,40 +1,51 @@
 """Class for the various animals in the game"""
 
 from pgu.vid import Sprite
+from pgu.algo import getline
 
 import imagecache
+import tiles
+from misc import Position
 
 class Animal(Sprite):
     """Base class for animals"""
 
-    def __init__(self, image_left, image_right, pos):
+    def __init__(self, image_left, image_right, tile_pos):
         # Create the animal somewhere far off screen
         Sprite.__init__(self, image_left, (-1000, -1000))
         self.image_left = image_left
         self.image_right = image_right
-        self.pos = pos
+        self.pos = Position(tile_pos[0], tile_pos[1])
 
     def loop(self, tv, _sprite):
-        ppos = tv.tile_to_view(self.pos)
+        ppos = tv.tile_to_view(self.pos.to_tuple())
         self.rect.x = ppos[0]
         self.rect.y = ppos[1]
 
     def move(self, state):
         """Given the game state, return a new position for the object"""
         # Default is not to move
-        return self.pos
+        pass
+
+    def _fix_face(self, final_pos):
+        """Set the face correctly"""
+        if final_pos.left_of(self.pos):
+            self.setimage(self.image_left)
+        elif final_pos.right_of(self.pos):
+            self.setimage(self.image_right)
 
 class Chicken(Animal):
     """A chicken"""
 
     def __init__(self, pos):
         image_left = imagecache.load_image('sprites/chkn.png')
-        image_right = imagecache.load_image('sprites/chkn.png', ("right_facing",))
+        image_right = imagecache.load_image('sprites/chkn.png',
+                ("right_facing",))
         Animal.__init__(self, image_left, image_right, pos)
 
     def move(self, gameboard):
         """A free chicken will move away from other free chickens"""
-        return self.pos
+        pass
 
 class Egg(Animal):
     """An egg"""
@@ -48,51 +59,165 @@
 class Fox(Animal):
     """A fox"""
 
+    costs = {
+            # weighting for movement calculation
+            'grassland' : 2,
+            'woodland' : 1, # Try to keep to the woods if possible
+            'broken fence' : 1,
+            'fence' : 10,
+            'guardtower' : 1, # We can pass under towers
+            'henhouse' : 1,
+            }
+
     def __init__(self, pos):
         image_left = imagecache.load_image('sprites/fox.png')
-        image_right = imagecache.load_image('sprites/fox.png', ("right_facing",))
-        self.full = False
+        image_right = imagecache.load_image('sprites/fox.png',
+                ("right_facing",))
         Animal.__init__(self, image_left, image_right, pos)
+        self.landmarks = [self.pos]
+        self.hunting = True
+        self.dig_pos = None
+        self.tick = 0
+
+    def _cost_path(self, path, gameboard):
+        """Calculate the cost of a path"""
+        total = 0
+        for pos in path:
+            if gameboard.in_bounds(pos):
+                this_tile = gameboard.tv.get(pos.to_tuple())
+                cost = self.costs.get(tiles.TILE_MAP[this_tile], 100)
+            else:
+                cost = 100 # Out of bounds is expensive
+            total += cost
+        return total
+
+    def _gen_path(self, start_pos, final_pos):
+        """Construct a direct path from start_pos to final_pos,
+           excluding start_pos"""
+        if abs(start_pos.x - final_pos.x) < 2 and \
+                abs(start_pos.y - final_pos.y) < 2:
+            # pgu gets this case wrong on occasion.
+            return [final_pos]
+        start = start_pos.to_tuple()
+        end = final_pos.to_tuple()
+        points = getline(start, end)
+        points.remove(start) # exclude start_pos
+        if end not in points:
+            # Rounding errors in getline cause this
+            points.append(end)
+        return [Position(x[0], x[1]) for x in points]
+
+    def _find_best_path_step(self, final_pos, gameboard):
+        """Find the cheapest path to final_pos, and return the next step
+           along the path."""
+        # We calculate the cost of the direct path
+        direct_path = self._gen_path(self.pos, final_pos)
+        min_cost = self._cost_path(direct_path, gameboard)
+        min_path = direct_path
+        # is there a point nearby that gives us a cheaper direct path?
+        poss = [Position(x, y) for x in range(self.pos.x - 2, self.pos.x + 3)
+                for y in range(self.pos.y - 2, self.pos.y + 3)]
+        for start in poss:
+            if start == self.pos:
+                continue # don't repeat work we don't need to
+            cand_path = self._gen_path(self.pos, start) + \
+                    self._gen_path(start, final_pos)
+            cost = self._cost_path(cand_path, gameboard)
+            if cost < min_cost:
+                min_cost = cost
+                min_path = cand_path
+        if not min_path:
+            return final_pos
+        return min_path[0]
+
+    def _find_path_to_woodland(self, gameboard):
+        """Dive back to woodland through the landmarks"""
+        # find the closest point to our current location in walked path
+        if self.pos == self.landmarks[-1]:
+            if len(self.landmarks) > 1:
+                self.landmarks.pop() # Moving to the next landmark
+            else:
+                # Safely back at the start
+                return self.pos
+        return self._find_best_path_step(self.landmarks[-1], gameboard)
+
+    def _find_path_to_chicken(self, gameboard):
+        """Find the path to the closest chicken"""
+        # Find the closest chicken
+        min_dist = 999
+        closest = None
+        for chicken in gameboard.chickens:
+            dist = chicken.pos.dist(self.pos)
+            if dist < min_dist:
+                min_dist = dist
+                closest = chicken
+        if not closest:
+            # No more chickens, so leave
+            self.hunting = False
+            return self.pos
+        if closest.pos == self.pos:
+            # Caught a chicken
+            gameboard.remove_chicken(closest)
+            self.hunting = False
+            return self.pos
+        return self._find_best_path_step(closest.pos, gameboard)
+
+    def _update_pos(self, gameboard, new_pos):
+        """Update the position, making sure we don't step on other foxes"""
+        final_pos = new_pos
+        moves = [Position(x, y) for x in range(self.pos.x-1, self.pos.x + 2)
+                for y in range(self.pos.y-1, self.pos.y + 2)]
+        blocked = False
+        for fox in gameboard.foxes:
+            if fox is not self and fox.pos == new_pos:
+                blocked = True
+            if fox.pos in moves:
+                moves.remove(fox.pos)
+        if blocked:
+            # find the closest point in moves to new_pos that's not a fence
+            final_pos = None
+            dist = 10
+            for poss in moves:
+                this_tile = gameboard.tv.get(poss.to_tuple())
+                new_dist = poss.dist(new_pos)
+                if new_dist < dist:
+                    dist = new_dist
+                    final_pos = poss
+        this_tile = gameboard.tv.get(final_pos.to_tuple())
+        if tiles.TILE_MAP[this_tile] == 'broken fence' and self.hunting:
+            # We'll head back towards the holes we make/find
+            self.landmarks.append(final_pos)
+        elif tiles.TILE_MAP[this_tile] == 'fence' and not self.dig_pos:
+            self.tick = 5
+            self.dig_pos = final_pos
+            return self.pos
+        return final_pos
 
     def move(self, gameboard):
         """Foxes will aim to move towards the closest henhouse or free
-          chicken"""
-        if self.full:
-            return
-        # Find the closest chicken
-        min_dist = 999
-        min_vec = None
-        closest = None
-        for chicken in gameboard.chickens:
-            vec = (chicken.pos[0] - self.pos[0], chicken.pos[1] - self.pos[1])
-            dist = abs(vec[0]) + abs(vec[1])
-            if dist < min_dist:
-                min_dist = dist
-                min_vec = vec
-                closest = chicken
-        xpos, ypos = self.pos
-        if min_vec[0] < 0:
-            xpos -= 1
-            self.setimage(self.image_left)
-        elif min_vec[0] > 0:
-            xpos += 1
-            self.setimage(self.image_right)
-        if min_vec[1] < 0:
-            ypos -= 1
-        elif min_vec[1] > 0:
-            ypos += 1
-        if closest.pos == self.pos:
-            gameboard.remove_chicken(closest)
-            self.full = True
-        for fox in gameboard.foxes:
-            if fox is not self:
-                if fox.pos[0] == xpos and fox.pos[1] == ypos:
-                    if xpos != self.pos[0]:
-                        xpos = self.pos[0]
-                    elif ypos != self.pos[1]:
-                        ypos = self.pos[1]
-                    else: # We move a step away
-                        xpos += 1
-        self.pos = (xpos, ypos)
+           chicken"""
+        if self.dig_pos:
+            if self.tick:
+                # We're digging through the fence
+                self.tick -= 1
+                # Check the another fox hasn't dug a hole for us
+                # We're top busy digging to notice if a hole appears nearby,
+                # but we'll notice if the fence we're digging vanishes
+                this_tile = gameboard.tv.get(self.dig_pos.to_tuple())
+                if tiles.TILE_MAP[this_tile] == 'broken fence':
+                    self.tick = 0 
+            else:
+                # We've dug through the fence, so make a hole
+                gameboard.tv.set(self.dig_pos.to_tuple(),
+                        tiles.REVERSE_TILE_MAP['broken fence'])
+                self.dig_pos = None
+            return 
+        if self.hunting:
+            desired_pos = self._find_path_to_chicken(gameboard)
+        else:
+            desired_pos = self._find_path_to_woodland(gameboard)
+        final_pos = self._update_pos(gameboard, desired_pos)
+        self._fix_face(final_pos)
+        self.pos = final_pos