From 2bc25e883ad32e05a101dd508135576ff404ac45 Mon Sep 17 00:00:00 2001 From: jbayati Date: Fri, 16 Jan 2026 07:56:21 -0500 Subject: [PATCH] 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. --- E/Proposal.md | 0 game.py | 393 ++++++++++++++++++ ghosts/__pycache__/ghosts.cpython-311.pyc | Bin 0 -> 23966 bytes ghosts/ghosts.py | 468 ++++++++++++++++++++++ poetry.lock | 36 +- proposal.md | 16 +- pyproject.toml | 20 +- 7 files changed, 894 insertions(+), 39 deletions(-) create mode 100644 E/Proposal.md create mode 100644 game.py create mode 100644 ghosts/__pycache__/ghosts.cpython-311.pyc create mode 100644 ghosts/ghosts.py 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 0000000000000000000000000000000000000000..c20c2944d37e90de9590f72b9e99fb046fe0c992 GIT binary patch literal 23966 zcmd^ndvF^^n%@lGAOHd+!M7-aBK4pwk&;YFww|U$Q7?;LOR{9kvLT2Ak(5Y+8i0Cm zhZcRSTZ1}Y7INqa-sR=sEA1YA)}EP^w@!DpccsnwE=p=s88DLwH5XQ?>athoO8(%+ zw~4crWb^xafWZKyWbZwyt~NvP>*w@*{q@&hcYj}Z|E|YV!r|HA2oLO-Cz0Z z|Mlhjf3mkXTRL-c-}?MH1n+QR?hG$*z>)`sCX@yI0Ih@_u(1N#fe{BxoWO-Y;}q=p zR*C99*dK|;j)jIpePMCzz)3OGdnp>|qsYL(a5NSg?hQu+pe28F3qitKHAllS)sc%+Efo17uUewv z!3$ABA@K8;+WR9z;r5H?L)THe_O5XBQYh@N5sg*aBnQw7s88-YSFCIHga8c1cL*}77UJ68p+(2i+1C)(X{{*T&A`@Zka00 zG&II7ilc6!rv82Fj1^IyjK2zwGmgK{i2%L1rl$Z=g;940Ja<6bixkVAlup-N47n2j zJ;Z;crxAmn;6CJgT1}z@>8dR>G7=sZ5G(r|3(~a*mDQ_} zG7=@1p@@liRK9?bP;sifSb-N>Eus^S=mJo!y@TP9C{pue>A?I2lqf;p5MzwD#;OZi zH=j_60-}2XKn<5yaW$m;^RCshYjvh+b-YV)ugti8Q^y{;8|U4PX?K(2Zj#+inVJ>x zuBpRu{4Lr!_ZnnhO0K`BcGs$I(rRrE3BrfjW;c&Sq6O8pXM4_=zo4$!*m|mHzS%vG z9JTX~+GKOuu~KoYlykqO_0XH$j}p-AXrMd?U(p+fXBdhr=rcxkbPYsD;%6Ak4{KRnj>C73l) zA&nXunH(BGCe?Mf2Tt01P(iHYAVWZ7))JDc!w|Q@pjr|P4n>5~LGrtU!ONqe!EA~> z7!)GC!Jz0v;bIkmS^~rhVt~Lp0?hNSjw9I+QM!{>hv0f=5> znJA6vqL3@%&T&hO@@5~2?<*Ps#;p>j4g>tSO|puABw4?TC>^((ILRd03&#?1XCF82 zknH15$@zApVZ3DA zCGcI`8>d*vJ?@c8B=;CfkX(|7+;Q)CsZ=U?FPO%Cl25W-;Y5C_1~oBDzCxU${8Wp? zN!~nGQfb~Vm@bhJqPMrB91AOFmt;phe7op-)+f}w=qo%oby~8{{SlkPn0r37w30XkRfy~>VKEwREmJK+ zp|P-PWh0a7?2Cj3gRyWF^P$La@O&gB3ab4)WW=Sgpz@bh%Lv|7&Ap={yrEdXY8l3~ zN9C`n{B_kNT*E)yVYtKBkw!xW^`SuVlbtf0zYq;x7#J4B1IR?PooEMY30Z6}ow%`3 zUU_@<^y;{M!QqLI#``8qr%ER}Gw!lPM|}50SEj_Z$W>eGGo{{d4cr=-yfk$wZqB&; z@#hmKCihJ3S>)`_noLDyyem^t1yEikm#@nB{C8aMx{_UKUz6f%l6_5?>iT5Y^jN~O zXyq!Zk!8`wdCI=^;;k1ad!~BgJ(-H?_)&}vla@Fi-<+x1yvVto^$bjQ$M+>nnaZk! zc>82>bMpB69SKu>A7*7qQ{p*%(p!;;#ih8E@d1(g#B<56+k2*g81Jd2C?xNjx|TFe zNy+HEca`h~dMx&8{6_o+`D-!WC;XE~GH%be4&6GGIH}s7w=%= zpgnu{noz!E|H*>v-q=#E_q2GESJ>1Lpf5}r=60opKo_yb= zw;?^;DWkm1Rj)`g;$1hShl|OJ(s8d~hV=Lb3;D*&ASrxIdU&NWCOs;~%cXLu0@B0J zWQM;G6UdB4$c&0SE>d~kFIaS$u|@JTnPG=(%1&+hsoheEU}fK_M7-k)A9YKWl0)+6 z%echqA#T>RnQiOHkPVTf`6Izbb*$BDtGFpWSjddCYNl56C@i_ z*~~_Q=MHBab|kEt^T>iJ452XysX}rUy=f|V(-2zvCRGkoE_07TDDtW~bUxZ@Qu&a| zpD&mr^8I6#A(DJd2<7KO{YOwvbThyNw}5Hk=ETd>9Z5dfG-J8_{N!6xZ++LC>P&_1 zcFmT}ZocRL;P3~fcT3aWHrdgp4a-anx8Z$}3ekkKjY(tG+8+*$Tvi?AjEqM6RXg)V zMwnz4pGE%GYO#Y}+X|psNFbQ=m`E)r?k2E@04+Sk7YK9`*hgSLfi3{mF&w^%Ox|$L`XJ0}B@C&7QVTNTSz*|IfLR+Y3QJQIgA6*bAG#QKS&nG)|JS8H9rP#>7tJzJTs zZ&&Ku(~cU*>5fEo#)awpt-bNR*&(3rX+uCD<)6OsWkW!fVF)O7u3zK;zH|uqrNw@# zW47aWcBOWa_{Y!`x_vEv2qQ&ur|eyW-|V`%*B|(mt=;qMyXE!WnU)O+Z8Xc&H>X0W z(5z)v_+aSnP`duvgk!;1IbE6@OixeNQgxYo%{ za$jYl3H1bYV$EEPSGjBE*SM>EjF#@%2Vb{9A6I}YUm&ND!&Z#n zMAFF#c4+#n<9wXMlFxC|F^)kmW%zFH3mhjniv{kuS+GjxAW@M$k~u}%lqFB`^tA|c zn`AD=eA)W%UQ(lyB{f=CRCY;>kmK@3WMROrc`|8a6lY|%CLmQ z%%N<`K`KWGW6sD_kAZV2AS|2m9P5t=fve#_FJiIO z9n=auJRIl~LnHkIh+^NNESTJ02#bUc7SO=;9HAYIV5)pBaA7di7l=k!ZH7Xq%3vfi z!fX%0cVj5Da zl~t!lHN(YZJoojJs*kA=87jfpz))CZ16*&e&B!HoWW(GJ@NLr1CvBN=8&=o(8pI(0Rg3FI1{2HrFH+R0Y4_&QwGdEi=ac!abTsvRVyR)Q+S zLKS0*RZU@m1-cL>@(4lHdu&}0ZP^E%bH2{`q1l-8oA^ZZC4es`xJ8q>Y;(q6bLY^z zhi)I4K9XR6Uo2DyFw~S`(w+kge$4k%YiB#s{!NO1lWh1cnvv;?FTRM9cJCXQUA-I6KT!t3uNlq8Ba@I8Sf!u_&!|&!G!sRVhn}Bhut=2>;Pb=y9OM z&wQN28;>PUCxx`VS+O_E_Vynf`s33NPCnc!A3v4eeOlRlI^FTA((x)%7EM-X8MKYG zz?eKTbtI0}MtQvZiv@4ZB4=`z<-ubtG`be8hy#&+R>E1`Se-nRjHN9tils%iwEWD$ zJ}g(Ro%N^fZHm23wr^WB`J9LOjC+Ng`z=~59<<0Zz|ED_-SNKbO_@_?)4nZ=Z%e!r zqhP#q>JTk~4&FLAd6@K{3+{?Umt41DHk5X6Qrw&5+)u;Wj1MkydH54=RU#%gJUe$X z?cJexcgWs-3%=^4MPAwQ1L2c_e{t!POAq#^w;fFT4k^Avvf;OA=6w5kEM3kDCw*;|H#i1+}Y{c@8JH_(aCjr&7YR?fCU{j*Udhsj%u*a zl)^3p8%@|kmYKpXdd3>IUbG=Qd&p)}oX#vbc370ij>9ZU0<1}AN`w+X7qcO`1vmU2 z!GjsHSG6+z+h?Tp`ix5M^=O4NND*fIe~#r0tsM)M>n5XRhS^+lRl;CkRHH+C!Uc(c z*&|sH(j^MZsaGjRI@s)UQ6Cj|{UKNkVGmOIF(_t*fhY}u zVhb{= z0S)h!FxEKzp(u$}7kR?L(c##@pz0w-m9FPjOGx(>jE;n^4yzvaZZsSorgXJ5A7s`* zEASkMsU>}6=nTfLj{uEhoo5cHmafCcPpH<@hr3Q5Qf-G096osHq*`+9rBerjC%$&z zK$mJC(rl%wTf<}s;$6J1`h;){hCmF0K@kJMXp||PTRqH*CW=H43lU3oYUUvzC6Z`h zi$K+a>PA)T)d2z8Hd}vqpsyduQO!}Ea99w3gR)K$_)UN)u`x!A{G;iwiD$=3S)bQ! zjNhUBzW{*3&0j;7#7u1inHm>Mxbg~kZ1AucPo|~`VNYhoIhTNpO*6Z{@1GSu z9{9b14=>%jly2RjwC+gP?^Nn{Qda1&Sdp3P6)3=+sYeL0iwym zj9CI3_fSG87RYwOHtNi~p#x{lL7hakLYo0y2=zGj5lpw3A=`WJynv zVzVylLt5{7kMJmLxqpYK=uH5$Txn%ONFJHBq)Rs{r5i~LU7BdVwIjacCwmS&5FQ?u zPrshtb5_}NR^HS9-qmDmMw&f^xk&X6rFw^q->?`zwBRn2%LicHIt&ND2WHKESfROc zuut=(-4_-2McI8ZQ$za#JaPPK+FyMX23FJ(j8o3RLnAoTU7D~Mg$B3Rbj?I_MUIPL ze7Hy+_RvI<K4{N@>)vwY&x zd|IsR*!dJP&EkU~6PJR!^eNsN5yfsAsE2i;f`~e4B(pyH!Vs4C>!u+L(0R4GsFn0p z-ZyI<8RWL~bgTS9rdz~Hi|Gcx38clh3CsW#l9?7I!@;Ye!Am(>=fW!Gr4`!*_t;zU zt?^sq$rEXBlj0@IrLz{Qw!~K5_VL)|oq9g;^3<**Kead6JMU_eT}>(BZUjy?XT{X@ zWEspEol`eb&GX*XvLm}eL8lJBMQqAgvkM-gd7c3olkyN-MtT@%C>iv|0%`ljR+QyKIe6~Uj49pTM#xWht zbChJy^IQS{nX_gFXvhT132v%Eu!4upk~JH@8xwTXQ?W2*v9K(Q1#N`TsZr1i9C?g{ zWw9W9T5UZ{QLfD;NpIdwdIV}w)rxr`6auVaotn_LkA!;J{LRz{Yhxd*jw5ifv&w=! zNF$ntn{?e#QfHyvwMQF_Jy6p?uX7<7rP46IqkI&_xV83Tje$h}}X#oggyjK-tkXsb=@z=h*S-e?iEA+xbjox?OYJ|7XIV_vO6^@#t4tkG8h{u+PA zVWp~Wdf-vjn)#|V>8f=~)jE=Tv?IgWm{sgo_!Gx(cO=V_Ti*94HpuRk`VY#YY}Tb@ zBvHd8+%s^Bcw6aIO3Q!^3o_Vw*PpTWVVx910H*T_&HyO=V>Vv}a%Dyc8A;Lw&Uq@Zjwn$k=nwGiu)xw#YOwPFzY3=Km} zL=B_$H5ikw#i*m^ByBGIe~~SE5CCJ3t325<(<-}~@k0**|D(^^%E-7PzIFKq@p3TMsGftC0>+&i^*U|PrzHp}xc zwqNN&ta!c}qY|?Z88M630ccoov#|txkI2CuGFKw3Ye2L)l=cqdiJ#dMtt%=)|LP_7 zuILcVlUz{GKb-yQ@@56WQdkRG6JYuRJPT-{EB$j=kUPoUvlhK=W+o8ZO^5OAi>R6Q zbQLM4u)|2#Q(*Wq7nWvBD=1Ib%oOGy10{^_NJVv{^jozsAJOV-$x|qu#0N2hz5`#^ zbv*Fv8Xi_#JFBg(=b5bi9@^m}y&W->TgCTdtMea}il`E*F`f4?c+8KZ3HAx1JMrk@f8Y!Nt%wwPT8L=vC39Q{rc`6z6WhWEf z*v5Q0#nWd^hlJV~c*Bs}P;%i{G10M$&lY~!p!q7Sr#|Ok)TqmqU(~mBZ5WA7P)FB? zp-%ccVs+75dai7{!aRs~_54M-bxg7nK>wz;1{-(8cIL2S5<=%b!LawuSZiba(oGOg zEX}}q1*I9{mzJi{Vpkw#IgYkfhrPI=X$Qv*nu1lWbTa}%!hzaOD-fhh?dxsgwf=q{D@Kz8uvW4EM1~x%PM_p zS*4OkD#Z?@k_12er^cGFm_I6@wMV0Mf){^3b}WZ6YEyc%U{rXrT!%iWuqIx~EB*&# z8;CzPhKwzRmirT9*qHBc*K)?%8s)38-L##Z#&Q9(W(AFT23QAd*D@E`LWrdva^q%!gmGY0Dwss@6 z#!I9Uab-abpFr&xsk^dIepX2DqldN`XFQmDU{UaqzA(UEM1#zd%Fws!q_WidTxmwv zn6t1HSo%-rbE0o9I~S5j-s`gU zgmRp>u;S=k#jhKdL`!C!6;g%hFQEK}T>Yeq!n)JW%`cgQ0iV3h?^kOMhEd!P>IZVv zf!{d7ZZ(FD`IgFQq%J>iab;%^`Ba0<)OO+1zFEmH{sm}jm^J2b!YEp_pH}>H$$NQAo#OC#rBwMuUaBnOCHlTzPjHQn8)5dV z%0pTDRz01rjE?{034OAdj#W>jBT{~4=}4OktG|Sb)l#+iBq~-HapV7&R5VHe@jFkF zD8^icR1?2pOdGFxA}1o{SCtb0uc*$vtd%*zg};KQH$R z@`X|P>eth+OUmmKGM?oRn-ZbvNY-2EXgWx}DnN+zk2DH=eguGsGif;9S$HfWKc*ty zFuTde!+1jFQOj7e#u8dtip==MYvvK0a~RQxLrJ+Kp^jaHp`r6aXwO*FlL?f~vWn=_ zjF&;52bNEr51Sr#{M!mubot0ctw8g?EDL7C$c9b+X)H!oN5+vPRueg zFnsAV7Pg#+F28_Vr~=R8|MN`%SrY>c1;i~E5u-f#4s*W7TEkL(CrwxQ zkIN+fwrR=_(+D*LsyhJ(CUsWbAHc621|bt?*48^Q!SwAvFWE(VrnJ+)%};6TTqf$A zQNj(aE!T&%U#e-Y^S)ns{^)$iQ5iq9{A=gruh9>LU@jo5zZk*01vR!Jg4OsOJbI1S zQ;mP$GHFgBcIi{BBK){EONj=!=Dc*s(rF!|&og<`(Ql(jMkMh8e1$X-e}t&9uR@F| zBF5Xut%-(ufkcB2Q)Le?KOC1s;YXq2`OvT&8A*pODx)mP!(0B-U~E!Sf!kcb(dCuuKYZOP^w*X1h`y z$##*N2#hsHjwV=DURXw~{ty|yN-tuxqh9nywi~TEDz7)+we* z%kqa)aO?v(fYdR}q^AhXt zpHfm$r>9N^8(X&5e{~N3HgWiW#>*OqSG-^b!|5Z%c9a3m1Cny-<9;KEAq2fXFE~YTl_xL|0L%q zaXLAOq+ak-3iVJgAV>e~3DZdmoYlI-i2|qJG(c%Xrpxr&km&{$*$)SiltBg?*~KVxp2||j_YLgw#PM&KI+-Xg&E^4=gHO{m$fUN+kiB?`U+pqht5 z*8m2vaggF@za6$+!_Y3$jsqO;$2MwgbyQ1^clLC>bgbvVi4)jcjqS)d%JeF|ah(7O zn$|jPn?;D;Gh>pEga6rELe!Gz)zCL>jxt208x}}3Xg_|Rc|%|*Sidcu3^53N#_+dABxuDQ|BNSmwjH)HY7OpwLxy5 zh#q0(!RSu3=NFKUk8w$n`;nt@-qDz{D1l9B$7aQ`S$1sxnT^Bola6G4Dx7w0P+S{m z!$<`?G=nX2imN^;%nT!CHg@mEylao_+M}g;E2hSA>cv@&qN*6d8L z+NG@8l`h})$I%Bn{_w^RZ=}n6;&yx#M}&6Y+C8~4_PDO*Y17LrF6YAU-L@b zvrxMtd2;5B+09DB#@XI`gZDSzi=-QNr)&2pwR`@o0;BL7@`;nmfmh^HuPLW!J*u1v zrw@eFofnkO3+dVm@}&sca**#N30T`{ihCZHRmN?&REiL4a<(ORGiUbLPbmc6!`{r* z^WL?xcP;Lhm|2mq-L}zoo5oc4eA#-rY(1{amauIL*j-6kyBmRY{b>bjwAbE9SG}oJy_qnhQj1)9SzGd1;w5-yqxWt+X!*2NZa<3O zGIgU*%{I*hXT`au4`XSZ9y{^L*#}LZ1RsbGn|>5aZ#^oncrkGxQ@b*Gd8Q`S^nP8c z_wL}_=DU$}?N*}0^6~k~{+(!%=!O`oSvfs^`^NN**^2q<4RZAcMjpcX`&aJ0b^rW> zvUK}?rG0<8VL#d`SD$6l78(MnvhVJnwcIP2+x($>?tHrCd8Os~bi)p%VMo%GPj=09 ze(0GK(=9ucmL2Jaol3(_l)S1Xbv{*-v?Xnisn;ZJINjl90;}%53)4flBh!)Zm&{jf zk*l_3aD1ctv8(K-T#d8-CylF8+uxUFB%BO{gqxD&#&+3Dzl<;N$hUUhw|3Ti&z<&d zReW1z-`0hS28h-H>go4w&`fybmxAevb4taztduEVB4z3uQ~b>Kgaex@m5SBq+GX2u zf?iOz9hzT%NM3*Fac;$%UH8UjJ7+_)`|n;yI9(r7>O+Yxh$@mk+ZUQ!@9v)Sr<=Da z&D)X=+~|_pj=h_-ebXSgHvD}Gu7za<7jC4nvrFUZag^Y?>{^E(2s`~+G6uP%RBp;{ zUaYR49#6fjRIi;irK{J^cHKL6ze(A!>;CcdhP`st3-N=vBVm#AIdMzPo$+_aQ>W9_ z87moL||ze}2t=dCh)EP<>^Lk)_CQT;x6Qe_D%h zdd*Q~%~35Y1|hrv!3CJGXDaGI;j*m@D_drUW)G%Ub|@=55(jY=-rWXG2Cry_5Iu%g zytDh=-M9CWeBwQ8A(}C6(%B(MPh{w!OY>fQ ztZcgfee%r__)7w$;A5X*(^GMXf&~5<{-ggMU_$>}EZIO8#ub9IJZCG-U#jv`GlDl? z)go-1&2#h3MU&0CVvz%&M1L;AXR>03paX})x(1_u4){&v(}BZlt;Do12UeL05gn9R zt+>`o2coh_hA%YLWgrEtUR&vE9GjFoXcM@B{_{s{Xz_|0mHBX_emJcp=`mp z*Zb*sTgE`iyppW&`{V9$7dDtUmd7>b$&*m9;wWDU4ou_V7*31e><2FQ!y4%${A*8f zUA|JOb{qQU{+rvxS6Ch#tRT9Ny-c37HSk|F+6O8yN>wy|U^jB(*Gu7g%h(M|Z@jQ=f9I0meG zdT|LwEF9F{6#oZ``Pa1QeFgw`U6s~N4o?kF9L#vU6Wu@Yl)cw-XXCpYQ|@&6Mx}gX z+S8_Z+9tXe>?QGSZ-4#f*Ap-Q+6~;(S6Y@>L)Y&mJ8wne5e%o_I(F+=a!smyW_{Yd zT5+$I-K&3Q=5UdgA=~4!>Lian{@vewHg)BLop*O)>c2->v1P9N{+3UU#P`Kb6Z;-p zO5Qp4YscPOophy6%&wXZ&H3kI_g{I?{qTtVYA{`XPANZ^wuBT*NVbGBdS-X3cXo^1 zzVAWT!xs5#ugIt0kb?vAB{5wdRm!7jOH8rEWJ_$B7dEHcyKrE$OFoXZLr@M~lCh30 zA63dn)0QiW<%(>%l5v$y9MfbN)1bTQ>aD>+?ZiZsUA8C=P!qjKfYdP}32}`vm~MzE zU`VEDvYy(YXb||hW0!V$=^pV5q@r!OD8_OQXHIxN!!^i-zYOP^V1F6TJ;DAmoM(dl zVJ2Q$K2fr0sp07=(d8i~<4FA*W9p6u-nYmVf*1H^iYx>#@@sgyY{LML^TQS%;(vMg VQx^X>#? literal 0 HcmV?d00001 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