from ghosts.ghosts import create_ghosts, _trigger_game_over import os import sys # ================= CONFIG ================= WIDTH = 40 HEIGHT = 15 MOVE_SPEED = 3 GHOST_RELEASE_DELAY = 50 CHASE_MEMORY = 30 # ========================================= RAW_MAP = [ "########################################", "#......................................#", "#.####.#####.#.##.#####.#####.#.##.#.###", "#............#............#........#.###", "#.#.####.###.#########.########.####.###", "#.#......#............................##", "#.######.#####################.######.##", "#.........####################........##", "#.#######.###GGGGGGGGGGGGG####.#########", "#.........###GGGGGGGGGGGGG####.........#", "#.####.#.########GGGGG###########.#.####", "#.#....#.#.............#........#...####", "#.#.####.###.######.####.#####.####.####", "#...................................####", "########################################", ] MAZE = RAW_MAP DIRS = { "left": (-1, 0), "right": (1, 0), "up": (0, -1), "down": (0, 1), } # ---------- Player start ---------- for y in range(HEIGHT): for x in range(WIDTH): if MAZE[y][x] == ".": START_POS = (x, y) break else: continue break def trigger_game_over(game): # delegate to the ghosts module helper which handles headless vs UI # and schedules a short delay for the UI. try: _trigger_game_over(game) except Exception: # fallback: attempt immediate end if getattr(game, 'ended', None) is None: setattr(game, 'ended', True) try: game.end() except Exception: setattr(game, 'game_over', True) def add_points(game, pts): """Add points to the game's canonical score storage. This updates common places where score may be kept so the final display always reads the same value. """ # primary: top-level attribute if hasattr(game, 'score'): try: game.score += pts except Exception: try: setattr(game, 'score', getattr(game, 'score', 0) + pts) except Exception: pass else: try: setattr(game, 'score', getattr(game, 'score', 0) + pts) except Exception: pass # secondary: if the game keeps a state dict-like object, update that too st = getattr(game, 'state', None) if isinstance(st, dict): st['score'] = st.get('score', 0) + pts class PacMan: name = "pacman" character = "C" display = True z = 3 def __init__(self): self.position = START_POS self.prev_position = START_POS self.current_dir = None self.buffered_dir = None self.last_move = -999 def handle_keystroke(self, key, game): if game.ended: return # guard against non-key events if key is None: return # extract a readable name from common key event shapes kn = None if isinstance(key, str): kn = key.lower() else: # prefer .char, then .name, then .key for attr in ('char', 'name', 'key'): val = getattr(key, attr, None) if val: kn = str(val).lower() break if not kn: return # ignore arrow keys explicitly if any(x in kn for x in ('left', 'right', 'up', 'down', 'arrow')): return # normalize to tokens and look for a single-letter wasd token import re tokens = [t for t in re.sub(r'[^a-z0-9]', '_', kn).split('_') if t] letter = None for t in tokens: if t in ('w', 'a', 's', 'd'): letter = t break # fallback: if kn itself is a single char and is wasd if letter is None and len(kn) == 1 and kn in ('w', 'a', 's', 'd'): letter = kn if not letter: return if letter == 'w': self.buffered_dir = 'up' elif letter == 'a': self.buffered_dir = 'left' elif letter == 's': self.buffered_dir = 'down' elif letter == 'd': self.buffered_dir = 'right' def can_move(self, d): dx, dy = DIRS[d] x, y = self.position[0] + dx, self.position[1] + dy return 0 <= x < WIDTH and 0 <= y < HEIGHT and MAZE[y][x] != "#" def play_turn(self, game): if game.ended: return if game.turn_number - self.last_move < MOVE_SPEED: return if self.buffered_dir and self.can_move(self.buffered_dir): self.current_dir = self.buffered_dir if self.current_dir and self.can_move(self.current_dir): dx, dy = DIRS[self.current_dir] self.prev_position = self.position self.position = (self.position[0] + dx, self.position[1] + dy) self.last_move = game.turn_number for agent in game.agents: if getattr(agent, "name", "").startswith("ghost_"): if agent.position == self.position: trigger_game_over(game) return class Wall: display = True character = "#" z = 0 def __init__(self, pos): self.position = pos self.name = f"wall_{pos}" class Pellet: display = True character = "." z = 1 def __init__(self, pos): self.position = pos self.name = f"pellet_{pos}" def play_turn(self, game): if game.ended: return pac = game.get_agent_by_name("pacman") if pac and pac.position == self.position: add_points(game, 10) game.remove_agent_by_name(self.name) # if there are no pellets left, we've collected them all -> max any_pellets = any(getattr(a, 'name', '').startswith('pellet_') for a in game.agents) if not any_pellets: setattr(game, 'maxed', True) # trigger the same game over flow as being caught by a ghost trigger_game_over(game) class GameOverWatcher: """Watches for a scheduled game-over timestamp and calls end() once the wall-clock time has passed. This is a fallback if threading.Timer isn't available; normally _trigger_game_over will start a timer. """ name = "gameover_watcher" display = False def play_turn(self, game): if getattr(game, 'ended', False): return if not getattr(game, 'game_over', False): return sched = getattr(game, '_game_over_scheduled', None) if sched is None: # no schedule found; call end immediately for safety try: game.end() except Exception: setattr(game, 'ended', True) return import time as _time if _time.time() >= sched: try: game.end() except Exception: setattr(game, 'ended', True) class GameOverlay: """Create temporary high-z agents that draw a centered overlay text when `game.game_over` is set. The overlay stays in place until the game actually ends. """ name = "game_overlay" display = False def __init__(self): self._created = False def _make_char_agent(self, ch, pos): # simple per-char agent class _C: display = True z = 100 def __init__(self, pos, ch): self.position = pos self.character = ch self.name = f"overlay_{pos[0]}_{pos[1]}" return _C(pos, ch) def play_turn(self, game): # create overlay once when game_over is flagged if getattr(game, 'game_over', False) and not getattr(game, '_overlay_present', False): # build two lines w = getattr(game, 'board_size', (WIDTH, HEIGHT))[0] if hasattr(game, 'board_size') else WIDTH h = getattr(game, 'board_size', (WIDTH, HEIGHT))[1] if hasattr(game, 'board_size') else HEIGHT mx = " [max]" if getattr(game, 'maxed', False) else "" lines = ["GAME OVER", f"SCORE: {getattr(game, 'score', 0)}{mx}"] start_y = h//2 - len(lines)//2 overlay_agents = [] for i, line in enumerate(lines): y = start_y + i x0 = max(0, w//2 - len(line)//2) for j, ch in enumerate(line): pos = (x0 + j, y) a = self._make_char_agent(ch, pos) overlay_agents.append(a) # attach overlay agents to the game and mark presence game.agents.extend(overlay_agents) game._overlay_present = True game._overlay_agents = overlay_agents # remove overlay agents once the game has ended if getattr(game, 'ended', False) and getattr(game, '_overlay_present', False): for a in list(getattr(game, '_overlay_agents', [])): try: # remove by name game.remove_agent_by_name(a.name) except Exception: try: game.agents.remove(a) except Exception: pass game._overlay_present = False game._overlay_agents = [] # ---------- Build agents ---------- agents = [PacMan()] ghost_spawns = [] for y, row in enumerate(MAZE): for x, ch in enumerate(row): if ch == "#": agents.append(Wall((x, y))) elif ch == ".": agents.append(Pellet((x, y))) elif ch == "G": ghost_spawns.append((x, y)) ghosts = create_ghosts( ghost_spawns, MAZE, DIRS, WIDTH, HEIGHT, GHOST_RELEASE_DELAY, MOVE_SPEED + 1 ) agents.extend(ghosts) # watcher to transition from flagged game_over to final end() after a short delay agents.append(GameOverWatcher()) agents.append(GameOverlay()) # ---------- Start ---------- HEADLESS = os.environ.get("HEADLESS") == "1" or "--headless" in sys.argv if HEADLESS: class HeadlessGame: def __init__(self, agents): self.agents = agents self.turn_number = 0 self.score = 0 self.ended = False def get_agent_by_name(self, name): for a in self.agents: if getattr(a, "name", None) == name: return a return None def remove_agent_by_name(self, name): self.agents = [a for a in self.agents if getattr(a, "name", None) != name] def end(self): print("\n===== GAME OVER =====") mx = " [max]" if getattr(self, 'maxed', False) else "" print(f"Final Score: {self.score}{mx}") print("=====================") self.ended = True def play(self, max_turns=500): while not self.ended and self.turn_number < max_turns: for agent in list(self.agents): if hasattr(agent, "play_turn"): agent.play_turn(self) self.turn_number += 1 HeadlessGame(agents).play() else: # import the graphical runtime only when running with UI from retro.game import Game from retro.agent import ArrowKeyAgent ArrowKeyAgent() game = Game(agents, {"score": 0}, board_size=(WIDTH, HEIGHT)) game.score = 0 game.ended = False print("Pac-Man | Correct Ghost AI") game.play() os.system("cls" if os.name == "nt" else "clear") print("\n" * 3) print("########################################") print("# #") print("# GAME OVER #") print("# #") mx = " [max]" if getattr(game, 'maxed', False) else "" print(f"# FINAL SCORE: {game.score:<6}{mx} #") print("# #") print("########################################")