comparison gamelib/animal.py @ 507:d3ceb9e9c48e

The fox move rewrite
author Neil Muller <drnlmuller@gmail.com>
date Thu, 26 Nov 2009 22:52:31 +0000
parents 3ed6c011106d
children 8fbff3505d1d
comparison
equal deleted inserted replaced
506:3b5717d742b2 507:d3ceb9e9c48e
10 import sound 10 import sound
11 import equipment 11 import equipment
12 import animations 12 import animations
13 import serializer 13 import serializer
14 import constants 14 import constants
15
16
17 NEIGHBOUR_4 = [Position(-1, 0), Position(1, 0), Position(0, 1), Position(0, -1)]
18
19
20 NEIGHBOUR_8 = [Position(-1, 0), Position(1, 0), Position(0, 1), Position(0, -1),
21 Position(1, 1), Position(1, -1), Position(-1, 1), Position(-1, -1)]
22
23
24 TILE_FENCE = tiles.REVERSE_TILE_MAP['fence']
15 25
16 class Animal(Sprite, serializer.Simplifiable): 26 class Animal(Sprite, serializer.Simplifiable):
17 """Base class for animals""" 27 """Base class for animals"""
18 28
19 STEALTH = 0 29 STEALTH = 0
78 88
79 def _game_death(self): 89 def _game_death(self):
80 # Call appropriate gameboard cleanup here. 90 # Call appropriate gameboard cleanup here.
81 pass 91 pass
82 92
83 def move(self, state): 93 def move(self):
84 """Given the game state, return a new position for the object""" 94 """Return a new position for the object"""
85 # Default is not to move 95 # Default is not to move
86 pass 96 pass
87 97
88 def attack(self): 98 def attack(self):
89 """Given the game state, attack a suitable target""" 99 """Given the game state, attack a suitable target"""
315 325
316 costs = { 326 costs = {
317 # weighting for movement calculation 327 # weighting for movement calculation
318 'grassland' : 2, 328 'grassland' : 2,
319 'woodland' : 1, # Try to keep to the woods if possible 329 'woodland' : 1, # Try to keep to the woods if possible
320 'broken fence' : 2, 330 'broken fence' : 1,
321 'fence' : 25, 331 'fence' : 25,
322 'guardtower' : 2, # We can pass under towers 332 'guardtower' : 2, # We can pass under towers
323 'henhouse' : 30, # Don't go into a henhouse unless we're going to 333 'henhouse' : 30, # Don't go into a henhouse unless we're going to
324 # catch a chicken there 334 # catch a chicken there
325 'hendominium' : 30, 335 'hendominium' : 30,
326 } 336 }
327 337
328 def __init__(self, pos, gameboard): 338 def __init__(self, pos, gameboard):
329 Animal.__init__(self, pos, gameboard) 339 Animal.__init__(self, pos, gameboard)
330 self.landmarks = [self.pos] 340 self.start_pos = self.pos
341 self.target = None
331 self.hunting = True 342 self.hunting = True
332 self.dig_pos = None 343 self.dig_pos = None
333 self.tick = 0 344 self.tick = 0
334 self.safe = False 345 self.safe = False
335 self.closest = None 346 self.closest = None
336 self.last_steps = []
337 # Foxes don't occupy places in the same way chickens do, but they 347 # Foxes don't occupy places in the same way chickens do, but they
338 # can still be inside 348 # can still be inside
339 self.building = None 349 self.building = None
350 self._last_steps = []
351 self.path = []
340 352
341 def outside(self): 353 def outside(self):
342 return self.building is None 354 return self.building is None
343 355
344 def _game_death(self): 356 def _game_death(self):
350 cost = self.costs.get(tiles.TILE_MAP[this_tile], 100) 362 cost = self.costs.get(tiles.TILE_MAP[this_tile], 100)
351 else: 363 else:
352 cost = 100 # Out of bounds is expensive 364 cost = 100 # Out of bounds is expensive
353 return cost 365 return cost
354 366
355 def _cost_path(self, path): 367 def _is_fence(self, pos):
356 """Calculate the cost of a path""" 368 if self.gameboard.in_bounds(pos):
357 total = 0 369 this_tile = self.gameboard.tv.get(pos.to_tile_tuple())
358 for pos in path: 370 return this_tile == TILE_FENCE
359 total += self._cost_tile(pos) 371 return False
360 return total 372
361 373 def _check_steps(self, step, border_func, end_func, max_steps):
362 def _gen_path(self, start_pos, final_pos): 374 steps = 0
363 """Construct a direct path from start_pos to final_pos, 375 cur_pos = self.pos
364 excluding start_pos""" 376 path = []
365 return start_pos.intermediate_positions(final_pos) 377 while steps < max_steps:
366 378 if not border_func(cur_pos):
367 def _find_best_path_step(self, final_pos): 379 # Not walking the edge
368 """Find the cheapest path to final_pos, and return the next step 380 # Is there a 4-NEIGHBOUR, to path[-1], not in the path that
369 along the path.""" 381 # is a continue
370 # We calculate the cost of the direct path 382 if not path:
371 if final_pos.z < self.pos.z: 383 # Rest of search will cover neighbours
372 # We need to try heading down. 384 return None
373 return Position(self.pos.x, self.pos.y, self.pos.z - 1) 385 for cand in [path[-1] + x for x in NEIGHBOUR_4]:
374 if final_pos.x == self.pos.x and final_pos.y == self.pos.y and \ 386 if cand in path:
375 final_pos.z > self.pos.z: 387 continue
376 # We try heading up 388 if border_func(cand):
377 return Position(self.pos.x, self.pos.y, self.pos.z + 1) 389 cur_pos = cand
378 cur_dist = final_pos.dist(self.pos) 390 break
379 if cur_dist < 2: 391 return None
380 # We're right ontop of our target, so just go there 392 path.append(cur_pos)
381 return final_pos 393 if end_func(cur_pos):
382 # Find the cheapest spot close to us that moves us closer to the target 394 # is there an 8-NEIGHBOUR that also satisfies end_func and is
383 neighbours = [Position(self.pos.x + x, self.pos.y + y, self.pos.z) for 395 # closer to target
384 x in range(-1, 2) for y in range(-1, 2) if (x, y) != (0, 0)] 396 dist = self.target.dist(cur_pos)
385 best_pos = self.pos 397 fin_pos = None
398 for pos in [cur_pos + x for x in NEIGHBOUR_8]:
399 if end_func(pos) and self.target.dist(pos) < dist:
400 fin_pos = pos
401 dist = self.target.dist(pos)
402 if fin_pos:
403 path.append(fin_pos)
404 return path
405 steps += 1
406 cur_pos = cur_pos + step
407 return None
408
409 def _search_for_path(self, border_func, end_func, max_steps):
410 paths = [None] * 4
411 paths[0] = self._check_steps(Position(-1, 0), border_func, end_func, max_steps)
412 paths[1] = self._check_steps(Position(0, -1), border_func, end_func, max_steps)
413 paths[2] = self._check_steps(Position(1, 0), border_func, end_func, max_steps)
414 paths[3] = self._check_steps(Position(0, 1), border_func, end_func, max_steps)
415 cands = [x for x in paths if x is not None]
416 if not cands:
417 return None
418 elif len(cands) == 1:
419 return cands[0][1:]
420 # take the end point closest to our target
421 final_path = cands[0]
422 min_dist = final_path[-1].dist(self.target)
423 for this_path in cands[1:]:
424 dist = this_path[-1].dist(self.target)
425 if dist < min_dist:
426 min_dist = dist
427 final_path = this_path
428 elif dist == min_dist and random.randint(0, 1) == 0:
429 final_path = this_path
430 return final_path[1:] # path's include self.pos
431
432 def _find_nearest_corner(self):
433 """Find the nearest corner of the bulding"""
434 COST_MARGIN = 25
435 def border(pos):
436 cost = self._cost_tile(pos)
437 if cost >= COST_MARGIN:
438 return False # in building isn't border
439 for tpos in [pos + step for step in NEIGHBOUR_8]:
440 if self.gameboard.in_bounds(tpos):
441 cost = self._cost_tile(tpos)
442 if cost >= COST_MARGIN:
443 return True
444 return False
445
446 def corner(pos):
447 # A corner is not 4-connected to a building
448 if not border(pos):
449 return False
450 for tpos in [pos + step for step in NEIGHBOUR_4]:
451 if self.gameboard.in_bounds(tpos):
452 cost = self._cost_tile(tpos)
453 if cost >= COST_MARGIN:
454 return False
455 return True
456
457 # We check left, then right and take the shortest if both are valid
458 return self._search_for_path(border, corner, 6)
459
460 def _find_fence_gap(self):
461 # We search for a gap in the fence
462 # we know we are next to fence. A gap in the fence is
463 # a point that borders the fence where the fence is not
464 # 4-connected
465
466 COST_MARGIN = 25
467
468 def border(pos):
469 if self._is_fence(pos) or self._cost_tile(pos) >= COST_MARGIN:
470 return False
471 for tpos in [pos + step for step in NEIGHBOUR_8]:
472 if self._is_fence(tpos) or self._cost_tile(tpos) >= COST_MARGIN:
473 print 'border pos', pos, self._cost_tile(pos)
474 return True
475 return False
476
477 def is_gap(pos):
478 # A gap neighbours only fence tiles which has < 2 4-neighbours
479 if self._is_fence(pos):
480 return False
481 fence_neighbours = [0]
482 for tpos in [pos + step for step in NEIGHBOUR_8]:
483 if self._is_fence(tpos):
484 connections = 0
485 for fpos in [tpos + step for step in NEIGHBOUR_4]:
486 if self.gameboard.in_bounds(fpos):
487 if self._is_fence(fpos) or self._cost_tile(tpos) >= COST_MARGIN:
488 # Expensive building is considered fence
489 connections += 1
490 else:
491 # Fence connecting to out of bounds counts as fence
492 connections += 1
493 fence_neighbours.append(connections)
494 return max(fence_neighbours) < 2
495
496 return self._search_for_path(border, is_gap, 7)
497
498 def _find_min_cost_neighbour(self, target):
499 """Find the minimum cost neighbour that's closer to target"""
500 cur_dist = target.dist(self.pos)
501 neighbours = [self.pos + step for step in NEIGHBOUR_8]
386 min_cost = 1000 502 min_cost = 1000
387 min_dist = cur_dist 503 min_dist = cur_dist
504 best = self.pos
388 for point in neighbours: 505 for point in neighbours:
389 dist = point.dist(final_pos) 506 if point in self._last_steps:
390 if dist < cur_dist: 507 continue
508 dist = point.dist(target)
509 if dist <= min_dist:
391 cost = self._cost_tile(point) 510 cost = self._cost_tile(point)
392 if cost < min_cost or (min_cost == cost and dist < min_dist): 511 if cost < min_cost or (min_cost == cost and dist < min_dist):
393 # Prefer closest of equal cost points 512 # Prefer closest of equal cost points
394 min_dist = dist 513 min_dist = dist
395 min_cost = cost 514 min_cost = cost
396 best = point 515 best = point
397 if min_cost < 20 or not self.gameboard.in_bounds(self.pos): 516 elif min_cost == cost and random.randint(0, 1) == 0:
517 # Be slightly non-deterministic when presented with
518 # equal choices
519 best = point
520 return best, min_cost
521
522 def _find_best_path_step(self):
523 """Find the cheapest path to final_pos, and return the next step
524 along the path."""
525 if self.path:
526 next_step = self.path.pop(0)
527 if next_step.dist(self.pos) < 2:
528 return next_step
529 else:
530 # Been bounced off the path
531 self.path = []
532 if self.target.z < self.pos.z:
533 # We need to try heading down.
534 return Position(self.pos.x, self.pos.y, self.pos.z - 1)
535 if self.target.x == self.pos.x and self.target.y == self.pos.y and \
536 self.target.z > self.pos.z:
537 # We try heading up
538 return Position(self.pos.x, self.pos.y, self.pos.z + 1)
539 cur_dist = self.target.dist(self.pos)
540 best, min_cost = self._find_min_cost_neighbour(self.target)
541 if cur_dist < 2:
542 # We're right ontop of our target, so just go there
543 return self.target
544 # Find the cheapest spot close to us that moves us closer to the target
545 if min_cost < 20 or not self.gameboard.in_bounds(self.pos) \
546 or not self.gameboard.in_bounds(best):
398 # If we're not on the gameboard yet, there's no point in looking 547 # If we're not on the gameboard yet, there's no point in looking
399 # for an optimal path. 548 # for an optimal path.
400 return best 549 return best
401 # Else expensive step, so think further 550 # Else expensive step, so think further
402 direct_path = self._gen_path(self.pos, final_pos) 551 if self._is_fence(best):
403 min_cost = self._cost_path(direct_path) 552 path = self._find_fence_gap()
404 min_path = direct_path 553 elif min_cost == 30:
405 # is there a point nearby that gives us a cheaper direct path? 554 # building
406 # This is delibrately not finding the optimal path, as I don't 555 path = self._find_nearest_corner()
407 # want the foxes to be too intelligent, although the implementation 556 else:
408 # isn't well optimised yet 557 # We're looping
409 # FIXME: Currently, this introduces loops, but the memory kind 558 self._last_steps = []
410 # avoids that. Fixing this is the next goal.
411 poss = [Position(self.pos.x + x, self.pos.y + y, self.pos.z) for
412 x in range(-3, 4) for y in range(-3, 4) if (x, y) != (0, 0)]
413 for start in poss:
414 cand_path = self._gen_path(self.pos, start) + \
415 self._gen_path(start, final_pos)
416 cost = self._cost_path(cand_path)
417 if cost < min_cost:
418 min_cost = cost
419 min_path = cand_path
420 if not min_path:
421 return final_pos
422 return min_path[0]
423
424 def _find_path_to_woodland(self):
425 """Dive back to woodland through the landmarks"""
426 # find the closest point to our current location in walked path
427 if self.pos == self.landmarks[-1]:
428 if len(self.landmarks) > 1:
429 self.landmarks.pop() # Moving to the next landmark
430 if not self.gameboard.in_bounds(self.pos) and not self.hunting:
431 # Safely out of sight
432 self.safe = True
433 return self.pos 559 return self.pos
434 return self._find_best_path_step(self.landmarks[-1]) 560 if path:
435 561 self.path = path[1:] # exclude 1st step
436 def _select_target(self): 562 return path[0]
563 return best
564
565 def _calc_next_move(self):
566 """Find the path to the target"""
567 if self.hunting:
568 # Check if we need to update our idea of a target
569 if not self.closest or self.closest not in self.gameboard.chickens:
570 # Either no target, or someone ate it
571 self._select_prey()
572 elif not self.target:
573 self.target = self.closest.pos
574 if not self.target:
575 self.target = self.start_pos
576 self._last_steps = []
577 if self.target == self.pos:
578 # No need to move, but we will need to update the target
579 self.target = None
580 return self.pos
581 if self.target.to_tile_tuple() == self.pos.to_tile_tuple():
582 # Only differ in z, so next step is in z
583 if self.target.z < self.pos.z:
584 new_z = self.pos.z - 1
585 else:
586 new_z = self.pos.z + 1
587 return Position(self.pos.x, self.pos.y, new_z)
588 return self._find_best_path_step()
589
590 def _select_prey(self):
437 min_dist = 999 591 min_dist = 999
438 self.closest = None 592 self.closest = None
439 for chicken in self.gameboard.chickens: 593 for chicken in self.gameboard.chickens:
440 dist = chicken.pos.dist(self.pos) 594 dist = chicken.pos.dist(self.pos)
441 if chicken.abode: 595 if chicken.abode:
443 if len(chicken.weapons()) > 0: 597 if len(chicken.weapons()) > 0:
444 dist += 5 # Prefer unarmed chickens 598 dist += 5 # Prefer unarmed chickens
445 if dist < min_dist: 599 if dist < min_dist:
446 min_dist = dist 600 min_dist = dist
447 self.closest = chicken 601 self.closest = chicken
448 602 self.target = chicken.pos
449 def _find_path_to_chicken(self):
450 """Find the path to the closest chicken"""
451 # Find the closest chicken
452 if self.closest not in self.gameboard.chickens:
453 # Either no target, or someone ate it
454 self._select_target()
455 if not self.closest: 603 if not self.closest:
456 # No more chickens, so leave 604 # No more chickens, so leave
457 self.hunting = False 605 self.hunting = False
606 self.target = self.start_pos
458 return self.pos 607 return self.pos
459 if self.closest.pos == self.pos:
460 # No need to move
461 return self.pos
462 if self.closest.pos.to_tile_tuple() == self.pos.to_tile_tuple():
463 # Only differ in z, so next step is in z
464 if self.closest.pos.z < self.pos.z:
465 new_z = self.pos.z - 1
466 else:
467 new_z = self.pos.z + 1
468 return Position(self.pos.x, self.pos.y, new_z)
469 return self._find_best_path_step(self.closest.pos)
470 608
471 def attack(self): 609 def attack(self):
472 """Attack a chicken""" 610 """Attack a chicken"""
473 chicken = self.gameboard.get_animal_at_pos(self.pos, 'chicken') 611 chicken = self.gameboard.get_animal_at_pos(self.pos, 'chicken')
474 if chicken: 612 if chicken:
478 def _catch_chicken(self, chicken): 616 def _catch_chicken(self, chicken):
479 """Catch a chicken""" 617 """Catch a chicken"""
480 chicken.damage() 618 chicken.damage()
481 self.closest = None 619 self.closest = None
482 self.hunting = False 620 self.hunting = False
483 self.last_steps = [] # Forget history here 621 self.target = self.start_pos
622 self._last_steps = []
484 623
485 def _update_pos(self, new_pos): 624 def _update_pos(self, new_pos):
486 """Update the position, making sure we don't step on other foxes""" 625 """Update the position, making sure we don't step on other foxes"""
626 if not self.hunting and not self.gameboard.in_bounds(self.pos):
627 self.safe = True
628 return self.pos
487 if new_pos == self.pos: 629 if new_pos == self.pos:
488 # We're not moving, so we can skip all the checks 630 # We're not moving, so we can skip all the checks
489 return new_pos 631 return new_pos
490 blocked = self.gameboard.get_animal_at_pos(new_pos, 'fox') is not None 632 blocked = self.gameboard.get_animal_at_pos(new_pos, 'fox') is not None
491 if not blocked and new_pos.z == self.pos.z:
492 # We're only worried about loops when not on a ladder
493 blocked = new_pos in self.last_steps
494 final_pos = new_pos 633 final_pos = new_pos
495 if blocked: 634 if blocked:
496 if new_pos.z != self.pos.z: 635 if new_pos.z != self.pos.z or self.pos.z != 0:
497 # We can only move up and down a ladder 636 # We can only move up and down a ladder
498 moves = [Position(self.pos.x, self.pos.y, z) for z 637 moves = [Position(self.pos.x, self.pos.y, z) for z
499 in range(self.pos.z-1, self.pos.z + 2) if z >= 0] 638 in range(self.pos.z-1, self.pos.z + 2) if z >= 0]
500 else: 639 else:
501 moves = [Position(x, y) for x in range(self.pos.x-1, self.pos.x + 2) 640 moves = [self.pos + step for step in NEIGHBOUR_8]
502 for y in range(self.pos.y-1, self.pos.y + 2)
503 if Position(x,y) != self.pos and
504 Position(x, y) not in self.last_steps and
505 self.pos.z == 0]
506 # find the cheapest point in moves that's not blocked 641 # find the cheapest point in moves that's not blocked
507 final_pos = None 642 final_pos = None
508 min_cost = 1000 643 min_cost = 1000
509 for poss in moves: 644 for poss in moves:
510 if self.gameboard.get_animal_at_pos(poss, 'fox'): 645 if self.gameboard.get_animal_at_pos(poss, 'fox'):
517 # Add some randomness in this case 652 # Add some randomness in this case
518 final_pos = poss 653 final_pos = poss
519 if not final_pos: 654 if not final_pos:
520 # No good choice, so stay put 655 # No good choice, so stay put
521 return self.pos 656 return self.pos
522 if self.gameboard.in_bounds(final_pos): 657 if self._is_fence(final_pos) and not self.dig_pos:
523 this_tile = self.gameboard.tv.get(final_pos.to_tile_tuple())
524 else:
525 this_tile = tiles.REVERSE_TILE_MAP['woodland']
526 if tiles.TILE_MAP[this_tile] == 'broken fence' and self.hunting:
527 # We'll head back towards the holes we make/find
528 self.landmarks.append(final_pos)
529 elif tiles.TILE_MAP[this_tile] == 'fence' and not self.dig_pos:
530 return self._dig(final_pos) 658 return self._dig(final_pos)
531 self.last_steps.append(final_pos) 659 self._last_steps.append(final_pos)
532 if len(self.last_steps) > 3: 660 if len(self._last_steps) > 6:
533 self.last_steps.pop(0) 661 self._last_steps.pop(0)
534 return final_pos 662 return final_pos
535 663
536 def _dig(self, dig_pos): 664 def _dig(self, dig_pos):
537 """Setup dig parameters, to be overridden if needed""" 665 """Setup dig parameters, to be overridden if needed"""
538 self.tick = 5 666 self.tick = 5
558 self.tick -= 1 686 self.tick -= 1
559 # We're still digging through the fence 687 # We're still digging through the fence
560 # Check the another fox hasn't dug a hole for us 688 # Check the another fox hasn't dug a hole for us
561 # We're too busy digging to notice if a hole appears nearby, 689 # We're too busy digging to notice if a hole appears nearby,
562 # but we'll notice if the fence we're digging vanishes 690 # but we'll notice if the fence we're digging vanishes
563 this_tile = self.gameboard.tv.get(self.dig_pos.to_tile_tuple()) 691 if not self._is_fence(self.dig_pos):
564 if tiles.TILE_MAP[this_tile] != 'fence':
565 self.tick = 0 692 self.tick = 0
566 else: 693 else:
567 # We've dug through the fence, so make a hole 694 # We've dug through the fence, so make a hole
568 self._make_hole() 695 self._make_hole()
569 return 696 return
570 elif self.hunting:
571 desired_pos = self._find_path_to_chicken()
572 else: 697 else:
573 desired_pos = self._find_path_to_woodland() 698 desired_pos = self._calc_next_move()
574 final_pos = self._update_pos(desired_pos) 699 final_pos = self._update_pos(desired_pos)
575 self._fix_face(final_pos) 700 self._fix_face(final_pos)
576 self.pos = final_pos 701 self.pos = final_pos
577 change_visible = False 702 change_visible = False
578 # See if we're entering/leaving a building 703 # See if we're entering/leaving a building
636 chicken.damage() 761 chicken.damage()
637 self.closest = None 762 self.closest = None
638 self.chickens_eaten += 1 763 self.chickens_eaten += 1
639 if self.chickens_eaten > 2: 764 if self.chickens_eaten > 2:
640 self.hunting = False 765 self.hunting = False
641 self.last_steps = [] 766 self.target = self.start_pos
767 self._last_steps = []
642 768
643 class Rinkhals(Fox): 769 class Rinkhals(Fox):
644 """The Rinkhals has eclectic tastes""" 770 """The Rinkhals has eclectic tastes"""
645 STEALTH = 80 771 STEALTH = 80
646 IMAGE_FILE = 'sprites/rinkhals.png' 772 IMAGE_FILE = 'sprites/rinkhals.png'
647 CONFIG_NAME = 'rinkhals' 773 CONFIG_NAME = 'rinkhals'
648 774
649 def _select_target(self): 775 costs = Fox.costs.copy()
776 costs['fence'] = 2
777
778 def _select_prey(self):
650 """The Rinkhals eats eggs""" 779 """The Rinkhals eats eggs"""
651 min_dist = 999 780 min_dist = 999
652 self.closest = None 781 self.closest = None
653 for chicken in self.gameboard.chickens: 782 for chicken in self.gameboard.chickens:
654 dist = chicken.pos.dist(self.pos) 783 dist = chicken.pos.dist(self.pos)
655 if not chicken.eggs: 784 if not chicken.eggs:
656 dist += 100 # The closest eggs have to be *far* away to be safe 785 dist += 100 # The closest eggs have to be *far* away to be safe
657 if dist < min_dist: 786 if dist < min_dist:
658 min_dist = dist 787 min_dist = dist
659 self.closest = chicken 788 self.closest = chicken
789 self.target = self.closest.pos
660 790
661 def _catch_chicken(self, chicken): 791 def _catch_chicken(self, chicken):
662 """The Rinkhals eats eggs, but does not harm chickens""" 792 """The Rinkhals eats eggs, but does not harm chickens"""
663 chicken.remove_eggs() 793 chicken.remove_eggs()
664 self.closest = None 794 self.closest = None
665 self.hunting = False 795 self.hunting = False
666 self.last_steps = [] 796 self.target = self.start_pos
797 self._last_steps = []
667 798
668 def _dig(self, dig_pos): 799 def _dig(self, dig_pos):
669 """Snakes ignore fences""" 800 """Snakes ignore fences"""
670 return dig_pos 801 return dig_pos
671 802