byte-vortex commited on
Commit
13397bf
Β·
verified Β·
1 Parent(s): b62987a

Deploy Myco from CI

Browse files
Files changed (1) hide show
  1. game/demo_roam.py +254 -0
game/demo_roam.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ demo_roam.py β€” Myco autonomous roaming demo.
3
+
4
+ Myco walks the 3x3 grid by itself, spawning mushrooms and reacting to them.
5
+ The LLM (engine._llm) is only called for narration, NOT for movement decisions.
6
+ All movement and game logic is pure Python.
7
+
8
+ Run: python demo_roam.py
9
+ """
10
+
11
+ import threading
12
+ import time
13
+ import random
14
+ import copy
15
+ import gradio as gr
16
+ from typing import Any, cast
17
+
18
+ from ui.renderers import (
19
+ forest_scene, dex_markdown, game_status_markdown,
20
+ mushroom_card, progress_markdown, _move
21
+ )
22
+ from game.engine import (
23
+ discover_mushroom, collect_current, study_current,
24
+ _is_poisonous, _score_collection,
25
+ )
26
+ from game.state import welcome_history
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Shared roam state β€” all mutations go through _STATE_LOCK
30
+ # ---------------------------------------------------------------------------
31
+ _STATE_LOCK = threading.Lock()
32
+
33
+ STATE = {
34
+ "position": (1, 1),
35
+ "current": None, # active mushroom dict
36
+ "active_mushrooms": [], # list of mushroom dicts with mush_x, mush_y
37
+ "collection": [],
38
+ "history": list(welcome_history()),
39
+ "thought": "The forest is waking up...",
40
+ "score": 0,
41
+ "health": 3,
42
+ "ticks": 0,
43
+ "visited": set(),
44
+ }
45
+
46
+ POISONOUS = {"Ghost Gill", "Pepper Pixie", "Ruby Knuckle", "Clockwork Chanterelle"}
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Pure Python roam logic β€” NO LLM decisions
50
+ # ---------------------------------------------------------------------------
51
+
52
+ def _next_pos(x, y, visited):
53
+ candidates = []
54
+ for dx, dy in [(0,-1),(0,1),(-1,0),(1,0)]:
55
+ nx, ny = x+dx, y+dy
56
+ if 0 <= nx <= 2 and 0 <= ny <= 2:
57
+ weight = 1 if (nx,ny) in visited else 4
58
+ candidates.extend([(nx,ny)] * weight)
59
+ return random.choice(candidates) if candidates else (x, y)
60
+
61
+
62
+ def _roam_tick():
63
+ """One autonomous tick β€” pure Python decision, optional LLM narration."""
64
+ with _STATE_LOCK:
65
+ x, y = STATE["position"]
66
+ collection = list(STATE["collection"])
67
+ current = STATE["current"]
68
+ mushrooms = list(STATE["active_mushrooms"])
69
+ visited = set(STATE["visited"])
70
+ ticks = STATE["ticks"]
71
+ STATE["ticks"] += 1
72
+
73
+ # --- Decision logic ---
74
+ action = "move"
75
+
76
+ # If there's a mushroom at our position
77
+ at_pos = [m for m in mushrooms if int(m.get("mush_x",-1)) == x
78
+ and int(m.get("mush_y",-1)) == y
79
+ and m.get("picked") != "Yes"]
80
+ if at_pos:
81
+ m = at_pos[0]
82
+ if m.get("poison") == "Yes":
83
+ action = "flee" # move away from poison
84
+ elif m.get("studied") != "Yes":
85
+ action = "study"
86
+ else:
87
+ action = "collect"
88
+
89
+ # If clearing is empty and we haven't searched here recently
90
+ elif (x, y) not in visited or ticks % 6 == 0:
91
+ action = "search"
92
+
93
+ # --- Execute ---
94
+ new_current = current
95
+ new_mushrooms = mushrooms
96
+ new_collection = collection
97
+ new_history = STATE["history"]
98
+ thought = STATE["thought"]
99
+
100
+ if action == "move" or action == "flee":
101
+ nx, ny = _next_pos(x, y, visited)
102
+ with _STATE_LOCK:
103
+ STATE["position"] = (nx, ny)
104
+ STATE["visited"].add((nx, ny))
105
+ return
106
+
107
+ if action == "search":
108
+ try:
109
+ mushroom, new_current, new_history = discover_mushroom(collection)
110
+ # Assign a grid position for the roaming sprite
111
+ new_current["mush_x"] = x
112
+ new_current["mush_y"] = y
113
+ new_current["picked"] = "No"
114
+ new_mushrooms = [m for m in mushrooms if not (
115
+ int(m.get("mush_x",-1)) == x and int(m.get("mush_y",-1)) == y
116
+ )] + [new_current]
117
+ thought = f"Found {new_current.get('name','?')}! πŸ„"
118
+ except Exception as e:
119
+ thought = "Something stirs in the moss..."
120
+
121
+ elif action == "study":
122
+ try:
123
+ studied_current, new_history = study_current(at_pos[0], STATE["history"])
124
+ if studied_current:
125
+ new_mushrooms = [studied_current if (
126
+ m.get("name") == at_pos[0].get("name")
127
+ ) else m for m in mushrooms]
128
+ new_current = studied_current
129
+ thought = f"Studied {studied_current.get('name','?')}. ✨"
130
+ except Exception as e:
131
+ thought = "Myco examines the mushroom carefully..."
132
+
133
+ elif action == "collect":
134
+ try:
135
+ new_collection, new_history = collect_current(at_pos[0], collection, STATE["history"])
136
+ # Mark as picked in the mushrooms list
137
+ new_mushrooms = [
138
+ {**m, "picked": "Yes"} if m.get("name") == at_pos[0].get("name") else m
139
+ for m in mushrooms
140
+ ]
141
+ new_current = {**at_pos[0], "picked": "Yes"}
142
+ thought = f"Collected {at_pos[0].get('name','?')}! +{at_pos[0].get('score_delta','?')} spores πŸŽ‰"
143
+ except Exception as e:
144
+ thought = "Added to the MycoDex..."
145
+
146
+ # Move after acting
147
+ nx, ny = _next_pos(x, y, visited)
148
+
149
+ with _STATE_LOCK:
150
+ STATE["position"] = (nx, ny)
151
+ STATE["visited"].add((nx, ny))
152
+ STATE["current"] = new_current
153
+ STATE["active_mushrooms"] = [m for m in new_mushrooms if m.get("picked") != "Yes"]
154
+ STATE["collection"] = new_collection
155
+ STATE["history"] = new_history[-20:] if new_history else []
156
+ STATE["thought"] = thought
157
+
158
+
159
+ def _roam_loop(interval=3.0):
160
+ while True:
161
+ try:
162
+ _roam_tick()
163
+ except Exception as e:
164
+ print(f"[Roam] tick error: {e}")
165
+ time.sleep(interval)
166
+
167
+
168
+ def start_roaming(interval=3.0):
169
+ t = threading.Thread(target=_roam_loop, args=(interval,), daemon=True)
170
+ t.start()
171
+ print("[Roam] Myco is now roaming autonomously.")
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Gradio poll functions
176
+ # ---------------------------------------------------------------------------
177
+
178
+ def poll_scene():
179
+ with _STATE_LOCK:
180
+ pos = STATE["position"]
181
+ current = STATE["current"]
182
+ mushrooms = list(STATE["active_mushrooms"])
183
+ coll = list(STATE["collection"])
184
+ return forest_scene(current, coll, pos, active_mushrooms=mushrooms)
185
+
186
+
187
+ def poll_status():
188
+ with _STATE_LOCK:
189
+ pos = STATE["position"]
190
+ current = STATE["current"]
191
+ coll = list(STATE["collection"])
192
+ return game_status_markdown(current, coll, pos)
193
+
194
+
195
+ def poll_thought():
196
+ with _STATE_LOCK:
197
+ return STATE["thought"]
198
+
199
+
200
+ def poll_dex():
201
+ with _STATE_LOCK:
202
+ coll = list(STATE["collection"])
203
+ return dex_markdown(coll) if coll else EMPTY_DEX
204
+
205
+
206
+ def poll_progress():
207
+ with _STATE_LOCK:
208
+ coll = list(STATE["collection"])
209
+ return progress_markdown(coll)
210
+
211
+
212
+ # ---------------------------------------------------------------------------
213
+ # Gradio UI
214
+ # ---------------------------------------------------------------------------
215
+
216
+ def build_roam_demo():
217
+ start_roaming(interval=3.0)
218
+
219
+ with gr.Blocks(title="Myco β€” Autonomous Roam Demo") as demo:
220
+ gr.HTML("""
221
+ <style>
222
+ .gradio-container { background: #1a1a2e !important; color: #f0ffe0; }
223
+ .progress-level, .progress-level-inner { display: none !important; }
224
+ </style>
225
+ """)
226
+
227
+ gr.Markdown("## πŸ„ Myco β€” Autonomous Roam Demo")
228
+ gr.Markdown(
229
+ "Myco walks the 3Γ—3 grid by itself every 3 seconds. "
230
+ "It searches clearings, studies mushrooms, and collects them β€” "
231
+ "all using pure Python logic. The LLM only adds narration text."
232
+ )
233
+
234
+ scene = gr.HTML(poll_scene())
235
+ thought = gr.Textbox(label="πŸ’¬ Myco's thought", interactive=False, value=poll_thought())
236
+
237
+ with gr.Row():
238
+ status = gr.Markdown(poll_status())
239
+ with gr.Column():
240
+ dex = gr.Markdown(poll_dex())
241
+ progress = gr.Markdown(poll_progress())
242
+
243
+ # Poll every 1.5s β€” fast enough to feel live, not so fast it hammers the server
244
+ demo.load(fn=poll_scene, outputs=scene, every=1.5)
245
+ demo.load(fn=poll_status, outputs=status, every=1.5)
246
+ demo.load(fn=poll_thought, outputs=thought, every=1.5)
247
+ demo.load(fn=poll_dex, outputs=dex, every=3.0)
248
+ demo.load(fn=poll_progress, outputs=progress, every=3.0)
249
+
250
+ return demo
251
+
252
+ if __name__ == "__main__":
253
+ demo = build_roam_demo()
254
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False)