I am proud of my game. I was stuck on the ghost movement logic for a while. I am worried that my game might be unbabalancd in difficulty. this hasn't sparked many interests mainly because of how restrited it was here to make a game using specific imports. I have learned a couple new skills such as how to use some variables and new functions.

This commit is contained in:
jbayati
2026-01-16 07:56:21 -05:00
parent c408a49c87
commit 2bc25e883a
7 changed files with 894 additions and 39 deletions

0
E/Proposal.md Normal file
View File

393
game.py Normal file
View File

@@ -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("########################################")

Binary file not shown.

468
ghosts/ghosts.py Normal file
View File

@@ -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

36
poetry.lock generated
View File

@@ -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"

View File

@@ -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.

View File

@@ -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