So this isn't a Python question exactly, but the code is in Python, so there it is. I have a mostly-working version of pacman -- currently, one monster v. pacman -- that includes the usual features: eat the power-up, chase the blue or flashing ghosts, get points.

When a ghost dies, his eyes zip back to the central monster box, where he is reborn and goes back to chasing pacman.

Here's the problem: my ghost algorithm is pretty simple, and it occasionally gets confused.

If he gets to a 'decision point' -- two or more options -- he asks the maze to give him paths to the next decision points by going up, down, right, or left. He then chooses the one that takes him closest to pacman (if chasing), farthest from pacman (if fleeing or flashing), or closest to the monster box (if recovering from having been eaten). He also has a random chance to go the wrong way.

However, if the ghost is recovering and happens to hit just the right spot directly over the monster box, he gets locked into an oscillation. The .bmp below shows the problem: if the ghost goes right, the end-point of that path is closer to the box than if he goes left. But once he's gone right, the path that ends closest to the box is to the left ... and so on.

The oscillation isn't permanent; eventually, the 'random chance' factor above causes him to get back to the monster box. Still, the oscillation point is annoying to the point of affecting game play.

So: anyone have suggestions for smarter algorithms? I would hate to kludge it by checking for that specific point...

Thanks,
Jeff

Dani AI

Generated

Nice fix, . A couple small tweaks can make the recovering-ghost path rock-solid without hard-coding special spots, and they play nicely with what suggested.

  • Plan on the intersection graph and commit. Build a grid/graph of walkable tiles or just the decision nodes, run BFS/A to the ghost box, and follow that route until you hit a blockage or the goal; then replan. This eliminates flip-flopping caused by re-evaluating only the next junction and gives you consistent paths. A on grids is simple to drop in and well documented. (redblobgames.com)

  • Add hysteresis: forbid a 180-degree reversal unless a mode change forces it (or you explicitly allow it). This single rule kills most two-node oscillations. It also mirrors classic Pac-Man behavior, where ghosts do not choose to reverse at intersections during normal travel. (destructoid.com)

  • Deterministic tie-breaks: when two choices have equal cost, pick from a fixed priority order (for example U,L,D,R). Determinism helps prevent ping-ponging near the box.

  • Light memory penalty: add a tiny cost for recently visited tiles so the planner prefers fresh ground over the last tile, without needing randomness.

Example next-step chooser you can call inside your mover, assuming a grid and Manhattan distance:

def next_dir(pos, target, grid, last_dir=None, allow_reverse=False):
    # tie-break order: U, L, D, R
    dirs = [(0,-1,'U'), (-1,0,'L'), (0,1,'D'), (1,0,'R')]
    rev = {'U':'D','D':'U','L':'R','R':'L'}
    best = None
    for dx,dy,d in dirs:
        if not allow_reverse and last_dir and d == rev[last_dir]: 
            continue
        nx, ny = pos[0]+dx, pos[1]+dy
        if grid[ny][nx] != '#':  # not a wall
            score = abs(nx-target[0]) + abs(ny-target[1])
            if d == last_dir: score += 0.05  # tiny hysteresis
            best = min(best, (score, d)) if best else (score, d)
    return best[1] if best else last_dir

Together, a committed shortest path plus no-immediate-reversal and deterministic tie-breaks will remove the oscillation over the box and keep your ghosts feeling smart without brittle special cases. (redblobgames.com)

Recommended Answers

All 3 Replies

Problem solved: rather than directing the recovering ghost to the decision point nearest the box, I compute the entire path from his current position to the box, as follows. The path thus generated will usually but not always be the shortest path; hence, it is 'pseudo-shortest.'

def direct_path(self, start, end):
        """
Return pseudo-shortest path from start to end.  start and end are point objects.
"""
        current = start
        paths = []
        while current != end:
            possibles = self.find_paths(current) # returns paths to nearest decision points
            for p in possibles[:]:
                # delete paths already traversed
                if p in paths:
                    possibles.remove(p)
                # special case -- the end point may not be a decision point, so we need to make sure path doesn't go past end point
                elif end in p:
                    index = p.index(end)
                    p = p[:index+1]
                    possibles = [p]
                    break

            # find shortest path
            possibles.sort(key=lambda x:\
                           self.distance(x[-1].coords,end.coords))
            
            paths.append(possibles[0])
            current = possibles[0][-1]  # last point in the path

        # list of paths now needs to be joined, understanding that the last
        # point in path n is same as first point in path n+1
        if not paths:
            return []
        final_path = paths[0]
        for p in paths[1:]:
            final_path.extend(p[1:])
        return final_path

Good thinking on your part Jeff!

Hi jrcagle,

I would have said to give the ghost a memory of his last choice, then exclude the opposite of that choice as a possibility when deciding which way to go in the present decision. In other words, if the ghost just decided to move east, don't let him move back west. But your solution is more elegant.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.