From 381b81d1225943e7bc9f61b563d8d5d31dbc716a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 09:10:07 +0000 Subject: [PATCH 1/3] Add Q-Learning AI to control Pac-Man autonomously MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces keyboard-driven Pac-Man with a tabular Q-Learning agent that learns to navigate the maze, eat pellets, and avoid ghosts through self-play. New file – pacman/q_learning_ai.py: • QLearningAgent class with epsilon-greedy policy, Q(s,a) update rule, and JSON persistence (q_table.json survives between runs). • 13-feature binary state: walls × 4, dangerous-ghost × 4, ghost vulnerable flag, pellet visible × 4. • Wall-aware action selection (never picks an immediately blocked move during exploitation). Changes to pacman/pacman.pyw: • AI_ENABLED = True flag at the top (set False to play manually). • AIStep() called every frame: makes a movement decision every AI_DECISION_INTERVAL (8) frames, computes rewards from score deltas (+10 pellet, +100 power pellet, etc.), applies –500 death penalty and +1000 level-win bonus, auto-restarts on game over, saves the Q-table every 5 episodes. • DrawAIStats() overlays episode count, epsilon, state count, and total steps in the top-left corner during play. • Manual keyboard input is preserved when AI_ENABLED = False. https://claude.ai/code/session_01EKGJKXQ5ahXkGuXTyAVZsA --- pacman/pacman.pyw | 142 ++++++++++++++++++++++-- pacman/q_learning_ai.py | 239 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 369 insertions(+), 12 deletions(-) create mode 100644 pacman/q_learning_ai.py diff --git a/pacman/pacman.pyw b/pacman/pacman.pyw index caa52f6..fe54a45 100644 --- a/pacman/pacman.pyw +++ b/pacman/pacman.pyw @@ -19,6 +19,16 @@ from pygame.locals import * # WIN??? SCRIPT_PATH=sys.path[0] +# --------------------------------------------------------------------------- +# Q-Learning AI configuration +# Set AI_ENABLED = False to play manually with the keyboard. +# --------------------------------------------------------------------------- +AI_ENABLED = True +AI_QTABLE_PATH = os.path.join(SCRIPT_PATH, 'q_table.json') + +if AI_ENABLED: + from q_learning_ai import QLearningAgent + # NO_GIF_TILES -- tile numbers which do not correspond to a GIF file # currently only "23" for the high-score list NO_GIF_TILES=[23] @@ -1371,12 +1381,102 @@ def CheckInputs(): elif thisGame.mode == 3: if pygame.key.get_pressed()[ pygame.K_RETURN ] or (js!=None and js.get_button(JS_STARTBUTTON)): thisGame.StartNewGame() - - + + +# _____________________________________ +# ___/ Q-Learning AI step (called per frame) \_______________________________________ + +# How many game frames between AI decisions (one tile = 16px / 2px-per-frame = 8 frames). +AI_DECISION_INTERVAL = 8 + +def AIStep(): + """Drive Pac-Man with the Q-Learning agent each frame.""" + global ai_prev_mode, ai_frame_counter + + # --- Mode 1: normal gameplay -- make periodic movement decisions --- + if thisGame.mode == 1: + ai_frame_counter += 1 + + if ai_frame_counter >= AI_DECISION_INTERVAL: + ai_frame_counter = 0 + + curr_state = ai_agent.get_state(player, ghosts, thisLevel, thisGame) + + # Update Q-table from the previous decision step + if ai_agent.prev_state is not None: + score_delta = thisGame.score - ai_agent.prev_score + reward = score_delta - 1 # small step penalty to encourage speed + ai_agent.update(ai_agent.prev_state, ai_agent.prev_action, reward, curr_state) + ai_agent.decay_epsilon() + + # Choose and immediately apply the next action + action = ai_agent.choose_action(curr_state, player, thisLevel) + speed = player.speed + dx, dy = ai_agent.ACTION_VELS[action] + dx *= speed + dy *= speed + + if not thisLevel.CheckIfHitWall(player.x + dx, player.y + dy, + player.nearestRow, player.nearestCol): + player.velX = dx + player.velY = dy + + ai_agent.prev_state = curr_state + ai_agent.prev_action = action + ai_agent.prev_score = thisGame.score + + # --- Mode 2: Pac-Man just died -- apply death penalty --- + elif thisGame.mode == 2: + if ai_prev_mode == 1 and ai_agent.prev_state is not None: + terminal = (0,) * 13 + ai_agent.update(ai_agent.prev_state, ai_agent.prev_action, -500, terminal) + ai_agent.decay_epsilon() + ai_agent.prev_state = None + + # --- Mode 3: game over -- save Q-table and auto-restart --- + elif thisGame.mode == 3: + ai_agent.episode += 1 + # Save every 5 episodes so progress is not lost + if ai_agent.episode % 5 == 0: + ai_agent.save(AI_QTABLE_PATH) + ai_agent.prev_state = None + thisGame.StartNewGame() + + # --- Mode 6: level complete -- apply level-win bonus --- + elif thisGame.mode == 6: + if ai_prev_mode == 1 and ai_agent.prev_state is not None: + terminal = (0,) * 13 + ai_agent.update(ai_agent.prev_state, ai_agent.prev_action, 1000, terminal) + ai_agent.prev_state = None + + ai_prev_mode = thisGame.mode + + # Escape: save Q-table and quit + if pygame.key.get_pressed()[pygame.K_ESCAPE]: + ai_agent.save(AI_QTABLE_PATH) + sys.exit(0) + + +def DrawAIStats(): + """Overlay a small HUD showing Q-learning progress.""" + info_lines = [ + "AI Q-Learning", + f"Episode : {ai_agent.episode}", + f"Epsilon : {ai_agent.epsilon:.3f}", + f"States : {len(ai_agent.q_table)}", + f"Steps : {ai_agent.steps}", + ] + x, y = 4, 4 + for line in info_lines: + surf = ai_font.render(line, True, (255, 255, 0)) + screen.blit(surf, (x, y)) + y += 10 + + # _____________________________________________ -# ___/ function: Get ID-Tilename Cross References \______________________________________ - +# ___/ function: Get ID-Tilename Cross References \______________________________________ + def GetCrossRef (): f = open(os.path.join(SCRIPT_PATH,"res","crossref.txt"), 'r') @@ -1473,20 +1573,34 @@ if pygame.joystick.get_count()>0: js.init() else: js=None -while True: +# --------------------------------------------------------------------------- +# Q-Learning AI initialisation +# --------------------------------------------------------------------------- +if AI_ENABLED: + ai_agent = QLearningAgent(qtable_path=AI_QTABLE_PATH) + ai_prev_mode = thisGame.mode # track mode transitions for reward signals + ai_frame_counter = 0 # counts frames between AI decisions + ai_font = pygame.font.Font(os.path.join(SCRIPT_PATH, "res", "VeraMoBd.ttf"), 9) + +while True: CheckIfCloseButton( pygame.event.get() ) - + + # Q-Learning AI handles all input and Q-table updates when enabled + if AI_ENABLED: + AIStep() + if thisGame.mode == 1: # normal gameplay mode - CheckInputs() - + if not AI_ENABLED: + CheckInputs() + thisGame.modeTimer += 1 player.Move() for i in range(0, 4, 1): ghosts[i].Move() thisFruit.Move() - + elif thisGame.mode == 2: # waiting after getting hit by a ghost thisGame.modeTimer += 1 @@ -1504,7 +1618,8 @@ while True: elif thisGame.mode == 3: # game over - CheckInputs() + if not AI_ENABLED: + CheckInputs() elif thisGame.mode == 4: # waiting to start @@ -1584,7 +1699,10 @@ while True: thisGame.DrawScore() - + + if AI_ENABLED: + DrawAIStats() + pygame.display.flip() - + clock.tick (60) diff --git a/pacman/q_learning_ai.py b/pacman/q_learning_ai.py new file mode 100644 index 0000000..999590f --- /dev/null +++ b/pacman/q_learning_ai.py @@ -0,0 +1,239 @@ +""" +q_learning_ai.py -- Q-Learning agent for Pac-Man + +State (13 binary features): + wall_u, wall_d, wall_l, wall_r -- wall in each direction + ghost_u, ghost_d, ghost_l, ghost_r -- dangerous ghost nearby in each direction + ghost_vulnerable -- power-pellet active + pellet_u, pellet_d, pellet_l, pellet_r -- pellet visible ahead in each direction + +Actions: 0=Up, 1=Down, 2=Left, 3=Right + +Q-update: Q(s,a) += alpha * (r + gamma * max Q(s') - Q(s,a)) +""" + +import random +import json +import os + + +class QLearningAgent: + + # Action constants + UP = 0 + DOWN = 1 + LEFT = 2 + RIGHT = 3 + + ACTIONS = [0, 1, 2, 3] + + # (delta_x_tiles, delta_y_tiles) for each action + ACTION_VELS = { + 0: (0, -1), # up: dy = -1 tile + 1: (0, 1), # down: dy = +1 tile + 2: (-1, 0), # left: dx = -1 tile + 3: ( 1, 0), # right: dx = +1 tile + } + + # Friendly names for display + ACTION_NAMES = {0: 'Up', 1: 'Down', 2: 'Left', 3: 'Right'} + + def __init__(self, + alpha=0.2, # learning rate + gamma=0.9, # discount factor + epsilon=1.0, # initial exploration rate + epsilon_min=0.05, # minimum exploration rate + epsilon_decay=0.9995, + qtable_path=None): + + self.alpha = alpha + self.gamma = gamma + self.epsilon = epsilon + self.epsilon_min = epsilon_min + self.epsilon_decay = epsilon_decay + self.qtable_path = qtable_path + + self.q_table = {} # state tuple -> {action int -> float} + + # Stats + self.episode = 0 + self.total_reward = 0.0 + self.steps = 0 + + # Carry-over between decision steps + self.prev_state = None + self.prev_action = None + self.prev_score = 0 + + if qtable_path and os.path.exists(qtable_path): + self.load(qtable_path) + + # ------------------------------------------------------------------ + # Q-table helpers + # ------------------------------------------------------------------ + + def _q(self, state, action): + return self.q_table.get(state, {}).get(action, 0.0) + + def _set_q(self, state, action, value): + if state not in self.q_table: + self.q_table[state] = {} + self.q_table[state][action] = value + + # ------------------------------------------------------------------ + # State computation + # ------------------------------------------------------------------ + + def get_state(self, player, ghosts, level_obj, game_obj): + """ + Return a compact binary state tuple describing Pac-Man's situation. + + Parameters + ---------- + player : pacman instance + ghosts : dict of ghost instances (indices 0-3 are the active ghosts) + level_obj : level instance + game_obj : game instance + """ + row = player.nearestRow + col = player.nearestCol + speed = player.speed + + DANGER_RADIUS = 5 # Manhattan-distance tiles for ghost danger + SCAN_DIST = 8 # tiles to scan ahead for pellets + + # ---- Walls (using the game's own hit-test) ---- + wall_u = int(level_obj.CheckIfHitWall(player.x, player.y - speed, row, col)) + wall_d = int(level_obj.CheckIfHitWall(player.x, player.y + speed, row, col)) + wall_l = int(level_obj.CheckIfHitWall(player.x - speed, player.y, row, col)) + wall_r = int(level_obj.CheckIfHitWall(player.x + speed, player.y, row, col)) + + # ---- Ghost danger ---- + # A ghost is dangerous when it is in state 1 (normal, chasing Pac-Man). + # State 2 = vulnerable (blue), state 3 = eaten (glasses, heading home). + ghost_u = ghost_d = ghost_l = ghost_r = 0 + ghost_vuln = int(game_obj.ghostTimer > 60) # >1 second remaining + + for i in range(4): + g = ghosts[i] + if g.state == 1: + dr = g.nearestRow - row + dc = g.nearestCol - col + dist = abs(dr) + abs(dc) + if dist <= DANGER_RADIUS: + # Assign to the dominant direction + if abs(dr) >= abs(dc): + if dr < 0: + ghost_u = 1 + else: + ghost_d = 1 + else: + if dc < 0: + ghost_l = 1 + else: + ghost_r = 1 + + # ---- Pellets in line of sight ---- + def scan(dr, dc): + for dist in range(1, SCAN_DIST + 1): + r = row + dr * dist + c = col + dc * dist + if level_obj.IsWall(r, c): + return 0 + tile = level_obj.GetMapTile(r, c) + if tile == 2 or tile == 3: # pellet or power pellet + return 1 + return 0 + + pellet_u = scan(-1, 0) + pellet_d = scan( 1, 0) + pellet_l = scan( 0,-1) + pellet_r = scan( 0, 1) + + return (wall_u, wall_d, wall_l, wall_r, + ghost_u, ghost_d, ghost_l, ghost_r, + ghost_vuln, + pellet_u, pellet_d, pellet_l, pellet_r) + + # ------------------------------------------------------------------ + # Action selection (epsilon-greedy, wall-aware) + # ------------------------------------------------------------------ + + def choose_action(self, state, player, level_obj): + """ + Select an action using an epsilon-greedy policy. + + Invalid actions (immediately blocked by a wall) are avoided so the + agent does not waste moves; if all four directions are blocked the + full action set is used as a fallback. + """ + speed = player.speed + row = player.nearestRow + col = player.nearestCol + + valid = [] + for action, (dx, dy) in self.ACTION_VELS.items(): + px = player.x + dx * speed + py = player.y + dy * speed + if not level_obj.CheckIfHitWall(px, py, row, col): + valid.append(action) + + if not valid: + valid = self.ACTIONS[:] + + if random.random() < self.epsilon: + return random.choice(valid) + + # Greedy: best Q-value among valid actions + return max(valid, key=lambda a: self._q(state, a)) + + # ------------------------------------------------------------------ + # Q-learning update + # ------------------------------------------------------------------ + + def update(self, state, action, reward, next_state): + """Apply the Q-learning (off-policy TD) update rule.""" + best_next = max(self._q(next_state, a) for a in self.ACTIONS) + current = self._q(state, action) + new_q = current + self.alpha * (reward + self.gamma * best_next - current) + self._set_q(state, action, new_q) + self.total_reward += reward + self.steps += 1 + + def decay_epsilon(self): + if self.epsilon > self.epsilon_min: + self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay) + + # ------------------------------------------------------------------ + # Persistence + # ------------------------------------------------------------------ + + def save(self, path): + """Serialise the Q-table to a JSON file.""" + data = { + 'epsilon': self.epsilon, + 'episode': self.episode, + 'steps': self.steps, + 'q_table': {} + } + for state, actions in self.q_table.items(): + key = ','.join(map(str, state)) + data['q_table'][key] = {str(a): v for a, v in actions.items()} + with open(path, 'w') as f: + json.dump(data, f) + print(f"[Q-Learning] Saved {len(self.q_table)} states eps={self.epsilon:.3f} ep={self.episode}") + + def load(self, path): + """Deserialise the Q-table from a JSON file.""" + try: + with open(path, 'r') as f: + data = json.load(f) + self.epsilon = data.get('epsilon', self.epsilon) + self.episode = data.get('episode', 0) + self.steps = data.get('steps', 0) + for key, actions in data.get('q_table', {}).items(): + state = tuple(int(x) for x in key.split(',')) + self.q_table[state] = {int(a): v for a, v in actions.items()} + print(f"[Q-Learning] Loaded {len(self.q_table)} states eps={self.epsilon:.3f} ep={self.episode}") + except Exception as e: + print(f"[Q-Learning] Could not load Q-table ({e}), starting fresh.") From 62336575f6c16b08471ed2310dbaa90babfb990f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 09:34:57 +0000 Subject: [PATCH 2/3] =?UTF-8?q?Add=20simulation=20speed=20control=20(+/-?= =?UTF-8?q?=20keys,=201x=E2=80=9316x)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pressing + speeds up the AI training simulation by running multiple game-update iterations per rendered frame (1x/2x/4x/8x/16x). Pressing - slows it back down. Current speed is shown in the AI HUD overlay. https://claude.ai/code/session_01EKGJKXQ5ahXkGuXTyAVZsA --- pacman/pacman.pyw | 187 +++++++++++++++++++++++++--------------------- 1 file changed, 103 insertions(+), 84 deletions(-) diff --git a/pacman/pacman.pyw b/pacman/pacman.pyw index fe54a45..53309a8 100644 --- a/pacman/pacman.pyw +++ b/pacman/pacman.pyw @@ -1466,6 +1466,7 @@ def DrawAIStats(): f"Epsilon : {ai_agent.epsilon:.3f}", f"States : {len(ai_agent.q_table)}", f"Steps : {ai_agent.steps}", + f"Speed : {sim_speed}x (+/- to change)", ] x, y = 4, 4 for line in info_lines: @@ -1576,6 +1577,9 @@ else: js=None # --------------------------------------------------------------------------- # Q-Learning AI initialisation # --------------------------------------------------------------------------- +sim_speed = 1 # simulation steps per rendered frame +SIM_SPEEDS = [1, 2, 4, 8, 16] # available speed levels (toggle with +/-) + if AI_ENABLED: ai_agent = QLearningAgent(qtable_path=AI_QTABLE_PATH) ai_prev_mode = thisGame.mode # track mode transitions for reward signals @@ -1584,95 +1588,110 @@ if AI_ENABLED: while True: - CheckIfCloseButton( pygame.event.get() ) + events = pygame.event.get() + CheckIfCloseButton(events) - # Q-Learning AI handles all input and Q-table updates when enabled + # Speed control: +/= to go faster, - to go slower (AI mode only) if AI_ENABLED: - AIStep() + for event in events: + if event.type == KEYDOWN: + if event.key in (K_EQUALS, K_PLUS): + idx = SIM_SPEEDS.index(sim_speed) + sim_speed = SIM_SPEEDS[min(idx + 1, len(SIM_SPEEDS) - 1)] + elif event.key == K_MINUS: + idx = SIM_SPEEDS.index(sim_speed) + sim_speed = SIM_SPEEDS[max(idx - 1, 0)] + + # Run the game update sim_speed times per rendered frame + for _step in range(sim_speed): + + # Q-Learning AI handles all input and Q-table updates when enabled + if AI_ENABLED: + AIStep() - if thisGame.mode == 1: - # normal gameplay mode - if not AI_ENABLED: - CheckInputs() + if thisGame.mode == 1: + # normal gameplay mode + if not AI_ENABLED: + CheckInputs() - thisGame.modeTimer += 1 - player.Move() - for i in range(0, 4, 1): - ghosts[i].Move() - thisFruit.Move() + thisGame.modeTimer += 1 + player.Move() + for i in range(0, 4, 1): + ghosts[i].Move() + thisFruit.Move() - elif thisGame.mode == 2: - # waiting after getting hit by a ghost - thisGame.modeTimer += 1 - - if thisGame.modeTimer == 90: - thisLevel.Restart() - - thisGame.lives -= 1 - if thisGame.lives == -1: - thisGame.updatehiscores(thisGame.score) - thisGame.SetMode( 3 ) - thisGame.drawmidgamehiscores() - else: - thisGame.SetMode( 4 ) - - elif thisGame.mode == 3: - # game over - if not AI_ENABLED: - CheckInputs() - - elif thisGame.mode == 4: - # waiting to start - thisGame.modeTimer += 1 - - if thisGame.modeTimer == 90: - thisGame.SetMode( 1 ) - player.velX = player.speed - - elif thisGame.mode == 5: - # brief pause after munching a vulnerable ghost - thisGame.modeTimer += 1 - - if thisGame.modeTimer == 30: - thisGame.SetMode( 1 ) - - elif thisGame.mode == 6: - # pause after eating all the pellets - thisGame.modeTimer += 1 - - if thisGame.modeTimer == 60: - thisGame.SetMode( 7 ) - oldEdgeLightColor = thisLevel.edgeLightColor - oldEdgeShadowColor = thisLevel.edgeShadowColor - oldFillColor = thisLevel.fillColor - - elif thisGame.mode == 7: - # flashing maze after finishing level - thisGame.modeTimer += 1 - - whiteSet = [10, 30, 50, 70] - normalSet = [20, 40, 60, 80] - - if not whiteSet.count(thisGame.modeTimer) == 0: - # member of white set - thisLevel.edgeLightColor = (255, 255, 255, 255) - thisLevel.edgeShadowColor = (255, 255, 255, 255) - thisLevel.fillColor = (0, 0, 0, 255) - GetCrossRef() - elif not normalSet.count(thisGame.modeTimer) == 0: - # member of normal set - thisLevel.edgeLightColor = oldEdgeLightColor - thisLevel.edgeShadowColor = oldEdgeShadowColor - thisLevel.fillColor = oldFillColor - GetCrossRef() - elif thisGame.modeTimer == 150: - thisGame.SetMode ( 8 ) - - elif thisGame.mode == 8: - # blank screen before changing levels - thisGame.modeTimer += 1 - if thisGame.modeTimer == 10: - thisGame.SetNextLevel() + elif thisGame.mode == 2: + # waiting after getting hit by a ghost + thisGame.modeTimer += 1 + + if thisGame.modeTimer == 90: + thisLevel.Restart() + + thisGame.lives -= 1 + if thisGame.lives == -1: + thisGame.updatehiscores(thisGame.score) + thisGame.SetMode( 3 ) + thisGame.drawmidgamehiscores() + else: + thisGame.SetMode( 4 ) + + elif thisGame.mode == 3: + # game over + if not AI_ENABLED: + CheckInputs() + + elif thisGame.mode == 4: + # waiting to start + thisGame.modeTimer += 1 + + if thisGame.modeTimer == 90: + thisGame.SetMode( 1 ) + player.velX = player.speed + + elif thisGame.mode == 5: + # brief pause after munching a vulnerable ghost + thisGame.modeTimer += 1 + + if thisGame.modeTimer == 30: + thisGame.SetMode( 1 ) + + elif thisGame.mode == 6: + # pause after eating all the pellets + thisGame.modeTimer += 1 + + if thisGame.modeTimer == 60: + thisGame.SetMode( 7 ) + oldEdgeLightColor = thisLevel.edgeLightColor + oldEdgeShadowColor = thisLevel.edgeShadowColor + oldFillColor = thisLevel.fillColor + + elif thisGame.mode == 7: + # flashing maze after finishing level + thisGame.modeTimer += 1 + + whiteSet = [10, 30, 50, 70] + normalSet = [20, 40, 60, 80] + + if not whiteSet.count(thisGame.modeTimer) == 0: + # member of white set + thisLevel.edgeLightColor = (255, 255, 255, 255) + thisLevel.edgeShadowColor = (255, 255, 255, 255) + thisLevel.fillColor = (0, 0, 0, 255) + GetCrossRef() + elif not normalSet.count(thisGame.modeTimer) == 0: + # member of normal set + thisLevel.edgeLightColor = oldEdgeLightColor + thisLevel.edgeShadowColor = oldEdgeShadowColor + thisLevel.fillColor = oldFillColor + GetCrossRef() + elif thisGame.modeTimer == 150: + thisGame.SetMode ( 8 ) + + elif thisGame.mode == 8: + # blank screen before changing levels + thisGame.modeTimer += 1 + if thisGame.modeTimer == 10: + thisGame.SetNextLevel() thisGame.SmartMoveScreen() From c85e2f1c84c61eb6795d0a4be42be964ac2020f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 09:41:33 +0000 Subject: [PATCH 3/3] Fix RecursionError in ghost pathfinding at high sim speeds FollowNextPathWay() recursed after finding a new path, but if FindPath returned an empty string (ghost already at destination), it would recurse infinitely. Guard both recursive calls with `if self.currentPath:` so they only fire when the new path is non-empty. This was latent in the original code but became reliably triggered at 2x+ sim speed. https://claude.ai/code/session_01EKGJKXQ5ahXkGuXTyAVZsA --- pacman/pacman.pyw | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pacman/pacman.pyw b/pacman/pacman.pyw index 53309a8..3b45928 100644 --- a/pacman/pacman.pyw +++ b/pacman/pacman.pyw @@ -676,13 +676,14 @@ class ghost (): if not self.state == 3: # chase pac-man self.currentPath = path.FindPath( (self.nearestRow, self.nearestCol), (player.nearestRow, player.nearestCol) ) - self.FollowNextPathWay() - + if self.currentPath: + self.FollowNextPathWay() + else: # glasses found way back to ghost box self.state = 1 self.speed = self.speed / 4 - + # give ghost a path to a random spot (containing a pellet) (randRow, randCol) = (0, 0) @@ -691,7 +692,8 @@ class ghost (): randCol = random.randint(1, thisLevel.lvlWidth - 2) self.currentPath = path.FindPath( (self.nearestRow, self.nearestCol), (randRow, randCol) ) - self.FollowNextPathWay() + if self.currentPath: + self.FollowNextPathWay() class fruit (): def __init__ (self):