diff --git a/E/Proposal.md b/E/Proposal.md new file mode 100644 index 0000000..e69de29 diff --git a/game.py b/game.py new file mode 100644 index 0000000..98476ba --- /dev/null +++ b/game.py @@ -0,0 +1,393 @@ +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("########################################") diff --git a/ghosts/__pycache__/ghosts.cpython-311.pyc b/ghosts/__pycache__/ghosts.cpython-311.pyc new file mode 100644 index 0000000..c20c294 Binary files /dev/null and b/ghosts/__pycache__/ghosts.cpython-311.pyc differ diff --git a/ghosts/ghosts.py b/ghosts/ghosts.py new file mode 100644 index 0000000..d96430f --- /dev/null +++ b/ghosts/ghosts.py @@ -0,0 +1,468 @@ +from collections import deque +import time +import heapq +import random + +# small randomness factor injected into ghost decision-making +# (keeps behavior mostly deterministic/chasing but adds slight variation) +RANDOMNESS = 0.12 + +class GhostManager: + """Tracks ghost instances and per-turn reservations to avoid collisions.""" + def __init__(self): + self.instances = [] + self.reserved_turn = -1 + self.reserved = set() + self.turn = -1 + + def register(self, g): + self.instances.append(g) + + def start_turn(self, turn): + if self.reserved_turn != turn: + self.reserved_turn = turn + self.reserved.clear() + self.turn = turn + + def reserve(self, pos): + self.reserved.add(pos) + + def is_reserved(self, pos): + return pos in self.reserved + + def occupied_positions(self, exclude=None): + return {g.position for g in self.instances if g is not exclude} + + +# path search helpers + +def bfs_find(maze, start, goal_test, on_board, blocked=None): + if blocked is None: + blocked = set() + q = deque([start]) + prev = {start: None} + while q: + cur = q.popleft() + if goal_test(cur): + path = [] + node = cur + while node is not None: + path.append(node) + node = prev[node] + path.reverse() + return path + x, y = cur + for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)): + nx, ny = x+dx, y+dy + npos = (nx, ny) + if npos in prev: + continue + if not on_board(npos): + continue + if maze[ny][nx] == '#': + continue + if npos in blocked: + continue + prev[npos] = cur + q.append(npos) + return None + + +def astar_find(maze, start, goal, on_board, blocked=None): + if blocked is None: + blocked = set() + def h(a,b): + return abs(a[0]-b[0]) + abs(a[1]-b[1]) + openq = [] + heapq.heappush(openq, (h(start, goal), 0, start)) + came_from = {start: None} + cost_so_far = {start: 0} + while openq: + _, cost, current = heapq.heappop(openq) + if current == goal: + path = [] + node = current + while node is not None: + path.append(node) + node = came_from[node] + path.reverse() + return path + x, y = current + for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)): + nx, ny = x+dx, y+dy + npos = (nx, ny) + if not on_board(npos): + continue + if maze[ny][nx] == '#': + continue + if npos in blocked and npos != goal: + continue + new_cost = cost + 1 + if npos not in cost_so_far or new_cost < cost_so_far[npos]: + cost_so_far[npos] = new_cost + priority = new_cost + h(npos, goal) + heapq.heappush(openq, (priority, new_cost, npos)) + came_from[npos] = current + return None + + +def _trigger_game_over(game): + """End the game in a way that's friendly to both headless and UI runs. + HeadlessGame defines an `ended` attribute and an `end()` method we can call. + In graphical runs we prefer to set a `game_over` flag so the main loop + / input handling stays active and the player can press a key to quit. + """ + # prefer headless behavior: if the object *looks* like HeadlessGame + # (it exposes an `ended` attribute) then call end() immediately. + if getattr(game, 'ended', None) is not None and callable(getattr(game, 'end', None)): + try: + game.end() + return + except Exception: + # fall back to flagging if end() misbehaves + pass + + # otherwise, set a flag the UI can react to and avoid calling end() + setattr(game, 'game_over', True) + + # schedule a background timer to call end() after a short delay + # so the UI has a small 'caught' moment before the final screen. + if getattr(game, '_game_over_timer', None) is None: + try: + import threading + + def _delayed_end(): + try: + game.end() + except Exception: + setattr(game, 'ended', True) + + t = threading.Timer(1.0, _delayed_end) + t.daemon = True + t.start() + setattr(game, '_game_over_timer', t) + except Exception: + # as a fallback, set a scheduled timestamp watched by GameOverWatcher + if getattr(game, '_game_over_scheduled', None) is None: + setattr(game, '_game_over_scheduled', time.time() + 1.0) + + +class Ghost: + # use a backing attribute and property so we can log unexpected + # changes to the display flag (this helps trace flicker/invisibility) + # keep the public API the same: `ghost.display` reads/writes still work. + + def __init__(self, name, char, start_pos, release_turn, maze, dirs, width, height, move_speed, manager, chase_memory=30, detection_radius=6): + self.name = name + self.character = char + # rendering order: higher z renders on top of lower z + # ensure ghosts are drawn above pellets/walls so they don't appear to "go invisible" + # rendering order: make ghosts above pellets/walls and PacMan to avoid occlusion + # set to 4 so ghosts render on top of PacMan (PacMan.z == 3) + self.z = 4 + # ensure instance-level display flag so renderers that check the + # instance attribute won't accidentally inherit a wrong value + # from other code paths + # use a private backing field; set it directly to avoid logging during construction + self._display = True + self.position = start_pos + self.release_turn = release_turn + # default to released so ghosts start roaming immediately + # (this makes them active from turn 0 instead of waiting for + # a delayed release). If you want delayed release again later, + # set release_turn to a positive value and change this flag. + self.released = True + self.current_dir = random.choice(list(dirs.keys())) + self.last_move = -999 + self.chasing = False + self.chase_until = -1 + # set prev_position to the starting position so renderers that + # interpolate between previous/current positions can do so + # even before the first move (helps smooth motion) + self.prev_position = start_pos + # once a ghost leaves the G-spawn area it should never re-enter + self.left_spawn = False + self.last_seen_pos = None + self.last_seen_turn = -999 + self.ghost_type = name.split('_')[-1] + + # references + self.MAZE = maze + self.DIRS = dirs + self.WIDTH = width + self.HEIGHT = height + self.MOVE_SPEED = move_speed + self.manager = manager + self.chase_memory = chase_memory + self.detection_radius = detection_radius + + manager.register(self) + + @property + def display(self): + return getattr(self, '_display', True) + + @display.setter + def display(self, val): + old = getattr(self, '_display', True) + if old != val: + # try to include turn info if manager has it; fall back to None + mgr_turn = getattr(self.manager, 'turn', None) if getattr(self, 'manager', None) is not None else None + print(f"[Ghost][DEBUG] {self.name} display changed {old} -> {val} at manager.turn={mgr_turn}") + self._display = val + + def on_board(self, pos): + x,y = pos + return 0 <= x < self.WIDTH and 0 <= y < self.HEIGHT + + def can_walk(self, pos): + # ensure position is on board before indexing the maze + if not self.on_board(pos): + return False + x, y = pos + # prevent re-entering the spawn area (G) after we've left it + if self.MAZE[y][x] == 'G' and self.left_spawn: + return False + return self.MAZE[y][x] != '#' + + def is_spawn(self): + x,y = self.position + return self.MAZE[y][x] == 'G' + + def line_of_sight(self, pac_pos): + gx, gy = self.position + px, py = pac_pos + if gx == px: + step = 1 if py > gy else -1 + for y in range(gy + step, py, step): + if self.MAZE[y][gx] == '#': + return False + return True + if gy == py: + step = 1 if px > gx else -1 + for x in range(gx + step, px, step): + if self.MAZE[gy][x] == '#': + return False + return True + return False + + def neighbors(self, pos): + x,y = pos + for dx, dy in self.DIRS.values(): + yield (x+dx, y+dy) + + def next_pos(self, dir_key): + """Return next position from current position following direction key.""" + if dir_key not in self.DIRS: + return None + dx, dy = self.DIRS[dir_key] + return (self.position[0] + dx, self.position[1] + dy) + + def play_turn(self, game): + # start turn bookkeeping + self.manager.start_turn(game.turn_number) + + # if the UI/game has already flagged game_over, don't take actions + if getattr(game, 'game_over', False): + return + + if game.turn_number - self.last_move < self.MOVE_SPEED: + return + + if not self.released: + if game.turn_number >= self.release_turn: + self.released = True + else: + return + + pac = game.get_agent_by_name('pacman') + if not pac: + return + + # occupied and blocked positions + occupied = self.manager.occupied_positions(exclude=self) + # avoid swapping: treat other ghosts' previous positions as blocked for this turn + swap_block = {g.prev_position for g in self.manager.instances if g is not self and getattr(g, 'prev_position', None) is not None} + blocked = set(occupied) | set(self.manager.reserved) | set(swap_block) + + # inside spawn: try to exit quickly + if self.is_spawn(): + path = bfs_find(self.MAZE, self.position, lambda p: self.MAZE[p[1]][p[0]] != 'G', self.on_board, blocked) + if path and len(path) > 1: + move = path[1] + if move not in blocked: + self.prev_position = self.position + self.position = move + # once we've moved out of a G tile, lock spawn re-entry + if not self.is_spawn(): + self.left_spawn = True + self.last_move = game.turn_number + self.manager.reserve(self.position) + if pac.position == self.position: + # end the game; UI will display a generic 'Game Over' + _trigger_game_over(game) + return + + # sensing + los = self.line_of_sight(pac.position) + if los: + self.last_seen_pos = pac.position + self.last_seen_turn = game.turn_number + self.chasing = True + self.chase_until = game.turn_number + self.chase_memory + + if self.chasing and game.turn_number > self.chase_until: + self.chasing = False + + seen_recently = (game.turn_number - self.last_seen_turn) <= self.chase_memory + within_radius = (abs(self.position[0]-pac.position[0]) + abs(self.position[1]-pac.position[1])) <= self.detection_radius + + target = None + if self.chasing or seen_recently or within_radius: + if self.ghost_type == 'red': + target = pac.position + elif self.ghost_type == 'pink': + if pac.current_dir and pac.current_dir in self.DIRS: + dx, dy = self.DIRS[pac.current_dir] + tx = pac.position[0] + dx*2 + ty = pac.position[1] + dy*2 + tx = max(0, min(self.WIDTH-1, tx)) + ty = max(0, min(self.HEIGHT-1, ty)) + target = (tx, ty) + else: + target = pac.position + elif self.ghost_type == 'blue': + red = next((g for g in self.manager.instances if g.ghost_type == 'red'), None) + if red and pac.current_dir and pac.current_dir in self.DIRS: + dx, dy = self.DIRS[pac.current_dir] + ahead = (pac.position[0] + dx*2, pac.position[1] + dy*2) + tx = ahead[0]*2 - red.position[0] + ty = ahead[1]*2 - red.position[1] + tx = max(0, min(self.WIDTH-1, tx)) + ty = max(0, min(self.HEIGHT-1, ty)) + target = (tx, ty) + else: + # unpredictable fallback + if random.random() < 0.5: + # pick random pellet or nearby tile + tx = pac.position[0] + random.randint(-3,3) + ty = pac.position[1] + random.randint(-3,3) + tx = max(0, min(self.WIDTH-1, tx)) + ty = max(0, min(self.HEIGHT-1, ty)) + target = (tx, ty) + else: + target = pac.position + + # add a tiny bit of randomness — occasionally ignore the target + # so ghosts feel less deterministic + if target and random.random() < RANDOMNESS: + target = None + + if target: + # allow moving into pac position + blocked_for_path = set(blocked) + if pac.position in blocked_for_path: + blocked_for_path.remove(pac.position) + path = astar_find(self.MAZE, self.position, target, self.on_board, blocked_for_path) + if path and len(path) > 1: + next_pos = path[1] + # avoid reversing + if self.prev_position and next_pos == self.prev_position: + # try alternative neighbors from the path (prefer non-reversing) + alts = [p for p in path[2:6] if p != self.prev_position and p not in blocked] + if alts: + next_pos = random.choice(alts) + if next_pos not in blocked: + self.prev_position = self.position + self.position = next_pos + # lock spawn re-entry if we left + if not self.is_spawn(): + self.left_spawn = True + self.last_move = game.turn_number + self.manager.reserve(self.position) + if pac.position == self.position: + _trigger_game_over(game) + return + + # fallback movement: try forward, else other options (avoid reverse) + forward = None + if self.current_dir in self.DIRS: + dx, dy = self.DIRS[self.current_dir] + forward = (self.position[0]+dx, self.position[1]+dy) + if forward and self.can_walk(forward) and forward not in blocked: + if self.prev_position and forward == self.prev_position: + # pick alternative + # build neighbor options from direction keys (safe next_pos helper) + options = [p for p in (self.next_pos(d) for d in self.DIRS) if p is not None and self.can_walk(p) and p not in blocked and p != self.prev_position] + if options: + forward = random.choice(options) + self.prev_position = self.position + self.position = forward + # lock spawn re-entry if we left + if not self.is_spawn(): + self.left_spawn = True + self.last_move = game.turn_number + self.manager.reserve(self.position) + if pac.position == self.position: + _trigger_game_over(game) + return + + options = [p for d in self.DIRS for p in [ (self.position[0]+self.DIRS[d][0], self.position[1]+self.DIRS[d][1]) ] if self.can_walk(p) and p not in blocked and p != self.prev_position] + if options: + chosen = random.choice(options) + self.prev_position = self.position + self.position = chosen + if not self.is_spawn(): + self.left_spawn = True + self.last_move = game.turn_number + self.manager.reserve(self.position) + if pac.position == self.position: + _trigger_game_over(game) + return + + # If we reached here, the ghost had no non-blocked options. To keep + # ghosts constantly roaming the map, try progressively weaker + # fallbacks instead of staying still: + # 1) try any walkable neighbor ignoring reservations (but avoid immediate reverse) + options_relaxed = [p for d in self.DIRS for p in [ (self.position[0]+self.DIRS[d][0], self.position[1]+self.DIRS[d][1]) ] if self.can_walk(p) and p != self.prev_position] + if options_relaxed: + chosen = random.choice(options_relaxed) + self.prev_position = self.position + self.position = chosen + if not self.is_spawn(): + self.left_spawn = True + self.last_move = game.turn_number + # still reserve our new position so others have a hint + self.manager.reserve(self.position) + if pac.position == self.position: + _trigger_game_over(game) + return + + # 2) as a last resort, allow reversing back to previous position so the ghost doesn't stall + if self.prev_position and self.can_walk(self.prev_position): + rev = self.prev_position + self.prev_position = self.position + self.position = rev + if not self.is_spawn(): + self.left_spawn = True + self.last_move = game.turn_number + self.manager.reserve(self.position) + if pac.position == self.position: + _trigger_game_over(game) + return + + +def create_ghosts(spawn_points, maze, dirs, width, height, ghost_release_delay, move_speed): + manager = GhostManager() + ghosts = [] + # choose three distinct spawn points if possible + unique = list(dict.fromkeys(spawn_points)) + if len(unique) < 3: + while len(unique) < 3: + unique.append(unique[-1]) + s0, s1, s2 = unique[0], unique[len(unique)//2], unique[-1] + + ghosts.append(Ghost('ghost_red', 'R', s0, 0, maze, dirs, width, height, move_speed, manager)) + ghosts.append(Ghost('ghost_blue', 'B', s1, ghost_release_delay, maze, dirs, width, height, move_speed, manager)) + ghosts.append(Ghost('ghost_pink', 'P', s2, ghost_release_delay*2, maze, dirs, width, height, move_speed, manager)) + return ghosts diff --git a/poetry.lock b/poetry.lock index 3fece54..6ea9dcd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. [[package]] name = "ansicon" @@ -15,21 +15,23 @@ files = [ [[package]] name = "blessed" -version = "1.20.0" +version = "1.25.0" description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." optional = false -python-versions = ">=2.7" +python-versions = ">=3.7" groups = ["main"] files = [ - {file = "blessed-1.20.0-py2.py3-none-any.whl", hash = "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058"}, - {file = "blessed-1.20.0.tar.gz", hash = "sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680"}, + {file = "blessed-1.25.0-py3-none-any.whl", hash = "sha256:e52b9f778b9e10c30b3f17f6b5f5d2208d1e9b53b270f1d94fc61a243fc4708f"}, + {file = "blessed-1.25.0.tar.gz", hash = "sha256:606aebfea69f85915c7ca6a96eb028e0031d30feccc5688e13fd5cec8277b28d"}, ] [package.dependencies] jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""} -six = ">=1.9.0" wcwidth = ">=0.1.4" +[package.extras] +docs = ["Pillow", "Sphinx (>3)", "sphinx-paramlinks", "sphinx_rtd_theme", "sphinxcontrib-manpage"] + [[package]] name = "jinxed" version = "1.3.0" @@ -61,31 +63,19 @@ files = [ [package.dependencies] blessed = ">=1.20.0,<2.0.0" -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - [[package]] name = "wcwidth" -version = "0.2.13" +version = "0.2.14" description = "Measures the displayed width of unicode strings in a terminal" optional = false -python-versions = "*" +python-versions = ">=3.6" groups = ["main"] files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, + {file = "wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1"}, + {file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"}, ] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "03cc38c17964eb2c920ecf014cbfcf966c0c719418a127947b33382f086a0a6e" +content-hash = "609c277eba88c4596adefc15c413c02733f0ae22a13b8742df3e77c0c52b2db3" diff --git a/proposal.md b/proposal.md index d53cb4b..fdfffb0 100644 --- a/proposal.md +++ b/proposal.md @@ -8,20 +8,18 @@ The name of my game will be ## Descirbe my version of my game - my game will be monopoly with countries cities as properties, it should have very simple graphics and easy to code - with print statements and more it should be fun if I add an AI to play against or something if I can. + my game will be pac-man, the ghosts will be G's and they will try to attack you while you grab tiny particles for points + and big particles to eat the ghosts for a limited amount of time for even more points. ## Core Mechanics it should have these core mechanics - -selling properties - -buying properties - -houses (3 houses --> hotel) - -A very simple trade option - I have about 6 weeks to work on this so i heavily belive I can make these work with using my recources and my - little knowledge and help from the teacher/AI at points. + - walking to eat particles + - point system + - high score system + - eating ghosts/particles for more points. ## Milestone(s) - My first step will be making the board and spaces, the next step will make spaces into properties, and the final step would be trading and polishing. + My first step will be making the map and then ghosts, the particles, then map selections and AI pathfinding ## Challenges I don't know what specific challenges I could face through this, but I don't think It will be easy either way. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7ce89ea..94646c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,27 @@ [project] -name = "project-game" +name = "Pac-Nom" version = "0.1.0" -description = "" +description = "Eat all the pellets and don't get caught" authors = [ - {name = "Chris Proctor",email = "chris@chrisproctor.net"} + {name="Jacob Bayati", email="jacobbayat28@lockportschools.net"} ] -license = {text = "MIT"} -readme = "README.md" requires-python = ">=3.10,<4.0" dependencies = [ - "retro-games (>=1.1.0,<2.0.0)" + "retro-games>=1.0.0", ] +[project.scripts] +play = "game.py" + +[tool.retro] +author = "Jacob" +description = "Eat all the pellets and do not get caught. if you get all of them you win!" +instructions = "Joystick for up,down,left and right" +result_file = "result.json" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] -package-mode = false +package-mode = false \ No newline at end of file