awacke1 commited on
Commit
c648bfe
ยท
verified ยท
1 Parent(s): 9e8778b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +112 -52
app.py CHANGED
@@ -32,16 +32,16 @@ st.set_page_config(
32
  # Game Config
33
  GAME_NAME = "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
34
  START_LOCATION = "Trailhead Camp โ›บ"
35
- CHARACTERS = {
36
- "Trailblazer Tim ๐ŸŒ„": {"voice": "en-US-GuyNeural", "desc": "Fearless hiker seeking epic trails!", "color": 0x00ff00},
37
- "Meme Queen Mia ๐Ÿ˜‚": {"voice": "en-US-JennyNeural", "desc": "Spreads laughs with wild memes!", "color": 0xff00ff},
38
- "Elk Whisperer Eve ๐ŸฆŒ": {"voice": "en-GB-SoniaNeural", "desc": "Talks to wildlife, loves nature!", "color": 0x0000ff},
39
- "Tech Titan Tara ๐Ÿ’พ": {"voice": "en-AU-NatashaNeural", "desc": "Codes her way through the Rockies!", "color": 0xffff00},
40
- "Ski Guru Sam โ›ท๏ธ": {"voice": "en-CA-ClaraNeural", "desc": "Shreds slopes, lives for snow!", "color": 0xffa500},
41
- "Cosmic Camper Cal ๐ŸŒ ": {"voice": "en-US-AriaNeural", "desc": "Stargazes and tells epic tales!", "color": 0x800080},
42
- "Rasta Ranger Rick ๐Ÿƒ": {"voice": "en-GB-RyanNeural", "desc": "Chills with natureโ€™s vibes!", "color": 0x00ffff},
43
- "Boulder Bro Ben ๐Ÿชจ": {"voice": "en-AU-WilliamNeural", "desc": "Climbs rocks, bro-style!", "color": 0xff4500}
44
- }
45
  FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
46
 
47
  # Prairie Simulator Locations
@@ -61,15 +61,25 @@ STATE_FILE = "user_state.txt"
61
  CHAT_FILE = os.path.join(CHAT_DIR, "quest_log.md")
62
  GAME_STATE_FILE = "game_state.json"
63
 
64
- # Cached Game State as "ML Model"
65
- @st.cache_data
66
  def load_game_state(_timestamp):
67
- """Load or initialize the game state, treated as a cached 'model'."""
68
  if os.path.exists(GAME_STATE_FILE):
69
  with open(GAME_STATE_FILE, 'r') as f:
70
  state = json.load(f)
71
  else:
72
- state = {"players": {}, "treasures": []}
 
 
 
 
 
 
 
 
 
 
73
  with open(GAME_STATE_FILE, 'w') as f:
74
  json.dump(state, f)
75
  return state
@@ -78,10 +88,24 @@ def update_game_state(state):
78
  """Update the game state and persist to file."""
79
  with open(GAME_STATE_FILE, 'w') as f:
80
  json.dump(state, f)
81
- # Clear cache to force reload with new timestamp
82
- load_game_state.clear()
83
  return load_game_state(time.time())
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  # Helpers
86
  def format_timestamp(username=""):
87
  now = datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -162,9 +186,13 @@ async def save_chat_entry(username, message, voice, is_markdown=False):
162
  await broadcast_message(f"{username}|{message}", "quest")
163
  st.session_state.chat_history.append(entry)
164
  st.session_state.last_transcript = message
165
- st.session_state.score += 10
166
- st.session_state.treasures += 1
167
- st.session_state.last_chat_update = time.time()
 
 
 
 
168
  return md_file, audio_file
169
 
170
  async def load_chat():
@@ -185,28 +213,26 @@ def init_session_state():
185
  'update_interval': 1, 'x_pos': 0, 'z_pos': 0, 'move_left': False,
186
  'move_right': False, 'move_up': False, 'move_down': False,
187
  'prairie_players': {}, 'last_chat_update': 0,
188
- 'game_state_timestamp': time.time()
 
189
  }
190
  for k, v in defaults.items():
191
  if k not in st.session_state:
192
  st.session_state[k] = v
193
  if st.session_state.username is None:
194
- saved_username = load_username()
195
- if saved_username and saved_username in CHARACTERS:
196
- st.session_state.username = saved_username
197
- else:
198
- st.session_state.username = random.choice(list(CHARACTERS.keys()))
199
- asyncio.run(save_chat_entry(st.session_state.username, "๐Ÿ—บ๏ธ Begins the Rocky Mountain Quest!", CHARACTERS[st.session_state.username]["voice"]))
200
  save_username(st.session_state.username)
201
- # Initialize player in cached game state if new
202
  game_state = load_game_state(st.session_state.game_state_timestamp)
203
  if st.session_state.username not in game_state["players"]:
204
  game_state["players"][st.session_state.username] = {
205
  "x": random.uniform(-20, 20),
206
  "z": random.uniform(-40, 40),
207
- "color": CHARACTERS[st.session_state.username]["color"],
208
  "score": 0,
209
- "treasures": 0
 
210
  }
211
  update_game_state(game_state)
212
 
@@ -227,7 +253,7 @@ async def websocket_handler(websocket, path):
227
  "username": username,
228
  "animal": "prairie_dog",
229
  "location": PRAIRIE_LOCATIONS["Deadwood, SD"],
230
- "color": CHARACTERS[username]["color"]
231
  }
232
  await broadcast_message(f"System|{username} joins the prairie!", room_id)
233
  else:
@@ -235,9 +261,10 @@ async def websocket_handler(websocket, path):
235
  game_state["players"][username] = {
236
  "x": random.uniform(-20, 20),
237
  "z": random.uniform(-40, 40),
238
- "color": CHARACTERS[username]["color"],
239
  "score": 0,
240
- "treasures": 0
 
241
  }
242
  update_game_state(game_state)
243
  st.session_state.players[client_id] = game_state["players"][username]
@@ -247,7 +274,7 @@ async def websocket_handler(websocket, path):
247
  async for message in websocket:
248
  if '|' in message:
249
  sender, content = message.split('|', 1)
250
- voice = CHARACTERS.get(sender, {"voice": "en-US-AriaNeural"})["voice"]
251
  game_state = load_game_state(st.session_state.game_state_timestamp)
252
  if content.startswith("MOVE:"):
253
  _, x, z = content.split(":")
@@ -256,19 +283,22 @@ async def websocket_handler(websocket, path):
256
  st.session_state.players[client_id]["z"] = z
257
  game_state["players"][sender]["x"] = x
258
  game_state["players"][sender]["z"] = z
 
259
  update_game_state(game_state)
260
  elif content.startswith("SCORE:"):
261
  score = int(content.split(":")[1])
262
  st.session_state.players[client_id]["score"] = score
263
  game_state["players"][sender]["score"] = score
 
264
  update_game_state(game_state)
265
  elif content.startswith("TREASURE:"):
266
  treasures = int(content.split(":")[1])
267
  st.session_state.players[client_id]["treasures"] = treasures
268
  game_state["players"][sender]["treasures"] = treasures
 
269
  for i, t in enumerate(game_state["treasures"]):
270
- if st.session_state.players[client_id]["x"] - 2 < t["x"] < st.session_state.players[client_id]["x"] + 2 and \
271
- st.session_state.players[client_id]["z"] - 2 < t["z"] < st.session_state.players[client_id]["z"] + 2:
272
  game_state["treasures"].pop(i)
273
  break
274
  update_game_state(game_state)
@@ -298,7 +328,15 @@ async def periodic_update():
298
  while True:
299
  if st.session_state.active_connections.get("quest"):
300
  game_state = load_game_state(st.session_state.game_state_timestamp)
301
- player_list = ", ".join([p["username"] for p in st.session_state.players.values()]) or "No adventurers yet!"
 
 
 
 
 
 
 
 
302
  message = f"๐Ÿ“ข Quest Update: Active Adventurers - {player_list}"
303
  player_data = json.dumps(list(game_state["players"].values()))
304
  await broadcast_message(f"MAP_UPDATE:{player_data}", "quest")
@@ -333,7 +371,7 @@ async def perform_arxiv_search(query, username):
333
  query, 5, "Semantic Search", "mistralai/Mixtral-8x7B-Instruct-v0.1", api_name="/update_with_rag_md"
334
  )[0]
335
  result = f"๐Ÿ“š Ancient Rocky Knowledge:\n{refs}"
336
- voice = CHARACTERS[username]["voice"]
337
  md_file, audio_file = await save_chat_entry(username, result, voice, True)
338
  return md_file, audio_file
339
 
@@ -370,6 +408,7 @@ rocky_map_html = f"""
370
  <div id="players">Players: 1</div>
371
  <div id="score">Score: 0</div>
372
  <div id="treasures">Treasures: 0</div>
 
373
  </div>
374
  <div id="chatBox"></div>
375
  <div id="controls">WASD/Arrows to move, Space to collect treasure</div>
@@ -407,11 +446,11 @@ rocky_map_html = f"""
407
  let treasures = [];
408
  const treasureMeshes = {{}};
409
  let xPos = 0, zPos = 0, moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
410
- let score = 0, treasureCount = 0;
411
 
412
  // Player initialization
413
  const playerGeometry = new THREE.BoxGeometry(2, 2, 2);
414
- const playerMaterial = new THREE.MeshPhongMaterial({{ color: {CHARACTERS[st.session_state.username]["color"]} }});
415
  const playerMesh = new THREE.Mesh(playerGeometry, playerMaterial);
416
  playerMesh.position.set(xPos, 1, zPos);
417
  playerMesh.castShadow = true;
@@ -435,7 +474,6 @@ rocky_map_html = f"""
435
  treasureMeshes[t.id].position.set(t.x, 1, t.z);
436
  }}
437
  }});
438
- // Remove treasures not in the data
439
  Object.keys(treasureMeshes).forEach(id => {{
440
  if (!treasureData.some(t => t.id === id)) {{
441
  scene.remove(treasureMeshes[id]);
@@ -454,6 +492,7 @@ rocky_map_html = f"""
454
  case 'ArrowDown': case 'KeyS': moveDown = true; break;
455
  case 'Space': collect = true; break;
456
  }}
 
457
  }});
458
  document.addEventListener('keyup', (event) => {{
459
  switch (event.code) {{
@@ -491,6 +530,8 @@ rocky_map_html = f"""
491
 
492
  camera.position.set(xPos, 50, zPos + 50);
493
  camera.lookAt(xPos, 0, zPos);
 
 
494
  }}
495
 
496
  function updatePlayers(playerData) {{
@@ -561,20 +602,16 @@ rocky_map_html = f"""
561
  def main():
562
  st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
563
  st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: {st.session_state.score} ๐Ÿ†")
564
- st.sidebar.write(f"๐Ÿ“œ {CHARACTERS[st.session_state.username]['desc']}")
565
  st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
566
  st.sidebar.write(f"๐Ÿ… Score: {st.session_state.score}")
567
  st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
568
  st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
569
 
570
- # User-controlled refresh interval
571
  st.session_state.update_interval = st.sidebar.slider("Refresh Interval (seconds)", 1, 10, 1, step=1)
572
-
573
- new_username = st.sidebar.selectbox("๐Ÿง™โ€โ™‚๏ธ Choose Your Hero", list(CHARACTERS.keys()), index=list(CHARACTERS.keys()).index(st.session_state.username))
574
- if new_username != st.session_state.username:
575
- asyncio.run(save_chat_entry(st.session_state.username, f"๐Ÿ”„ Transforms into {new_username}!", CHARACTERS[st.session_state.username]["voice"]))
576
- st.session_state.username = new_username
577
- save_username(st.session_state.username)
578
  st.rerun()
579
 
580
  left_col, right_col = st.columns([2, 1])
@@ -586,21 +623,23 @@ def main():
586
  message = st.text_input(f"๐Ÿ—จ๏ธ {st.session_state.username} says:", placeholder="Speak or type to chat! ๐ŸŒฒ")
587
  if st.button("๐ŸŒŸ Send & Chat ๐ŸŽค"):
588
  if message:
589
- voice = CHARACTERS[st.session_state.username]["voice"]
590
  md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
591
  if audio_file:
592
  play_and_download_audio(audio_file)
 
593
  st.success(f"๐ŸŒ„ +10 points! New Score: {st.session_state.score}")
594
  mycomponent = components.declare_component("speech_component", path="./speech_component")
595
  val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
596
  if val and val != st.session_state.last_transcript:
597
  val_stripped = val.strip().replace('\n', ' ')
598
  if val_stripped:
599
- voice = CHARACTERS.get(st.session_state.username, {"voice": "en-US-AriaNeural"})["voice"]
600
  st.session_state['speech_processed'] = True
601
  md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
602
  if audio_file:
603
  play_and_download_audio(audio_file)
 
604
  st.rerun()
605
 
606
  with right_col:
@@ -608,6 +647,16 @@ def main():
608
  prairie_map = folium.Map(location=[44.0, -103.0], zoom_start=8, tiles="CartoDB Positron")
609
  for loc, (lat, lon) in PRAIRIE_LOCATIONS.items():
610
  folium.Marker([lat, lon], popup=loc).add_to(prairie_map)
 
 
 
 
 
 
 
 
 
 
611
  for client_id, player in st.session_state.prairie_players.items():
612
  folium.CircleMarker(
613
  location=player['location'],
@@ -624,14 +673,25 @@ def main():
624
  if st.button("Move"):
625
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:ANIMAL:{animal}", "quest"))
626
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:MOVE:{location}", "quest"))
 
627
  st.rerun()
628
 
629
  elapsed = time.time() - st.session_state.last_update
630
  remaining = max(0, st.session_state.update_interval - elapsed)
631
  st.sidebar.markdown(f"โณ Next Update in: {int(remaining)}s")
632
- if remaining <= 0:
 
 
 
 
 
 
 
 
 
 
633
  st.session_state.last_update = time.time()
634
- st.session_state.game_state_timestamp = time.time() # Update timestamp to refresh cache
635
  st.rerun()
636
 
637
  if not st.session_state.get('server_running', False):
 
32
  # Game Config
33
  GAME_NAME = "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
34
  START_LOCATION = "Trailhead Camp โ›บ"
35
+ CHARACTERS = [
36
+ {"name": "Trailblazer Tim ๐ŸŒ„", "voice": "en-US-GuyNeural", "desc": "Fearless hiker seeking epic trails!", "color": 0x00ff00},
37
+ {"name": "Meme Queen Mia ๐Ÿ˜‚", "voice": "en-US-JennyNeural", "desc": "Spreads laughs with wild memes!", "color": 0xff00ff},
38
+ {"name": "Elk Whisperer Eve ๐ŸฆŒ", "voice": "en-GB-SoniaNeural", "desc": "Talks to wildlife, loves nature!", "color": 0x0000ff},
39
+ {"name": "Tech Titan Tara ๐Ÿ’พ", "voice": "en-AU-NatashaNeural", "desc": "Codes her way through the Rockies!", "color": 0xffff00},
40
+ {"name": "Ski Guru Sam โ›ท๏ธ", "voice": "en-CA-ClaraNeural", "desc": "Shreds slopes, lives for snow!", "color": 0xffa500},
41
+ {"name": "Cosmic Camper Cal ๐ŸŒ ", "voice": "en-US-AriaNeural", "desc": "Stargazes and tells epic tales!", "color": 0x800080},
42
+ {"name": "Rasta Ranger Rick ๐Ÿƒ", "voice": "en-GB-RyanNeural", "desc": "Chills with natureโ€™s vibes!", "color": 0x00ffff},
43
+ {"name": "Boulder Bro Ben ๐Ÿชจ", "voice": "en-AU-WilliamNeural", "desc": "Climbs rocks, bro-style!", "color": 0xff4500}
44
+ ]
45
  FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
46
 
47
  # Prairie Simulator Locations
 
61
  CHAT_FILE = os.path.join(CHAT_DIR, "quest_log.md")
62
  GAME_STATE_FILE = "game_state.json"
63
 
64
+ # Cached Game State as "ML Model" using st.cache_resource
65
+ @st.cache_resource
66
  def load_game_state(_timestamp):
67
+ """Load or initialize the game state, treated as a cached resource."""
68
  if os.path.exists(GAME_STATE_FILE):
69
  with open(GAME_STATE_FILE, 'r') as f:
70
  state = json.load(f)
71
  else:
72
+ state = {
73
+ "players": {},
74
+ "treasures": [
75
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
76
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
77
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
78
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
79
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
80
+ ],
81
+ "history": []
82
+ }
83
  with open(GAME_STATE_FILE, 'w') as f:
84
  json.dump(state, f)
85
  return state
 
88
  """Update the game state and persist to file."""
89
  with open(GAME_STATE_FILE, 'w') as f:
90
  json.dump(state, f)
91
+ load_game_state.clear() # Clear cache to force reload
 
92
  return load_game_state(time.time())
93
 
94
+ def reset_game_state():
95
+ """Reset the game state to initial conditions."""
96
+ state = {
97
+ "players": {},
98
+ "treasures": [
99
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
100
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
101
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
102
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
103
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
104
+ ],
105
+ "history": []
106
+ }
107
+ return update_game_state(state)
108
+
109
  # Helpers
110
  def format_timestamp(username=""):
111
  now = datetime.now().strftime("%Y%m%d_%H%M%S")
 
186
  await broadcast_message(f"{username}|{message}", "quest")
187
  st.session_state.chat_history.append(entry)
188
  st.session_state.last_transcript = message
189
+ game_state = load_game_state(st.session_state.game_state_timestamp)
190
+ game_state["history"].append(entry)
191
+ if username in game_state["players"]:
192
+ game_state["players"][username]["score"] += 10
193
+ game_state["players"][username]["treasures"] += 1
194
+ game_state["players"][username]["last_active"] = time.time()
195
+ update_game_state(game_state)
196
  return md_file, audio_file
197
 
198
  async def load_chat():
 
213
  'update_interval': 1, 'x_pos': 0, 'z_pos': 0, 'move_left': False,
214
  'move_right': False, 'move_up': False, 'move_down': False,
215
  'prairie_players': {}, 'last_chat_update': 0,
216
+ 'game_state_timestamp': time.time(), 'name_index': 0,
217
+ 'timeout': 60, 'auto_refresh': 30, 'last_activity': time.time()
218
  }
219
  for k, v in defaults.items():
220
  if k not in st.session_state:
221
  st.session_state[k] = v
222
  if st.session_state.username is None:
223
+ st.session_state.name_index = (st.session_state.name_index + 1) % len(CHARACTERS)
224
+ st.session_state.username = CHARACTERS[st.session_state.name_index]["name"]
225
+ asyncio.run(save_chat_entry(st.session_state.username, "๐Ÿ—บ๏ธ Joins the Rocky Mountain Quest!", CHARACTERS[st.session_state.name_index]["voice"]))
 
 
 
226
  save_username(st.session_state.username)
 
227
  game_state = load_game_state(st.session_state.game_state_timestamp)
228
  if st.session_state.username not in game_state["players"]:
229
  game_state["players"][st.session_state.username] = {
230
  "x": random.uniform(-20, 20),
231
  "z": random.uniform(-40, 40),
232
+ "color": CHARACTERS[st.session_state.name_index]["color"],
233
  "score": 0,
234
+ "treasures": 0,
235
+ "last_active": time.time()
236
  }
237
  update_game_state(game_state)
238
 
 
253
  "username": username,
254
  "animal": "prairie_dog",
255
  "location": PRAIRIE_LOCATIONS["Deadwood, SD"],
256
+ "color": CHARACTERS[st.session_state.name_index]["color"]
257
  }
258
  await broadcast_message(f"System|{username} joins the prairie!", room_id)
259
  else:
 
261
  game_state["players"][username] = {
262
  "x": random.uniform(-20, 20),
263
  "z": random.uniform(-40, 40),
264
+ "color": CHARACTERS[st.session_state.name_index]["color"],
265
  "score": 0,
266
+ "treasures": 0,
267
+ "last_active": time.time()
268
  }
269
  update_game_state(game_state)
270
  st.session_state.players[client_id] = game_state["players"][username]
 
274
  async for message in websocket:
275
  if '|' in message:
276
  sender, content = message.split('|', 1)
277
+ voice = next(c["voice"] for c in CHARACTERS if c["name"] == sender)
278
  game_state = load_game_state(st.session_state.game_state_timestamp)
279
  if content.startswith("MOVE:"):
280
  _, x, z = content.split(":")
 
283
  st.session_state.players[client_id]["z"] = z
284
  game_state["players"][sender]["x"] = x
285
  game_state["players"][sender]["z"] = z
286
+ game_state["players"][sender]["last_active"] = time.time()
287
  update_game_state(game_state)
288
  elif content.startswith("SCORE:"):
289
  score = int(content.split(":")[1])
290
  st.session_state.players[client_id]["score"] = score
291
  game_state["players"][sender]["score"] = score
292
+ game_state["players"][sender]["last_active"] = time.time()
293
  update_game_state(game_state)
294
  elif content.startswith("TREASURE:"):
295
  treasures = int(content.split(":")[1])
296
  st.session_state.players[client_id]["treasures"] = treasures
297
  game_state["players"][sender]["treasures"] = treasures
298
+ game_state["players"][sender]["last_active"] = time.time()
299
  for i, t in enumerate(game_state["treasures"]):
300
+ if abs(st.session_state.players[client_id]["x"] - t["x"]) < 2 and \
301
+ abs(st.session_state.players[client_id]["z"] - t["z"]) < 2:
302
  game_state["treasures"].pop(i)
303
  break
304
  update_game_state(game_state)
 
328
  while True:
329
  if st.session_state.active_connections.get("quest"):
330
  game_state = load_game_state(st.session_state.game_state_timestamp)
331
+ # Remove inactive players (timeout after 60 seconds)
332
+ current_time = time.time()
333
+ inactive_players = [p for p, data in game_state["players"].items() if current_time - data["last_active"] > st.session_state.timeout]
334
+ for player in inactive_players:
335
+ del game_state["players"][player]
336
+ await broadcast_message(f"System|{player} timed out after 60 seconds!", "quest")
337
+ update_game_state(game_state)
338
+
339
+ player_list = ", ".join([p for p in game_state["players"].keys()]) or "No adventurers yet!"
340
  message = f"๐Ÿ“ข Quest Update: Active Adventurers - {player_list}"
341
  player_data = json.dumps(list(game_state["players"].values()))
342
  await broadcast_message(f"MAP_UPDATE:{player_data}", "quest")
 
371
  query, 5, "Semantic Search", "mistralai/Mixtral-8x7B-Instruct-v0.1", api_name="/update_with_rag_md"
372
  )[0]
373
  result = f"๐Ÿ“š Ancient Rocky Knowledge:\n{refs}"
374
+ voice = next(c["voice"] for c in CHARACTERS if c["name"] == username)
375
  md_file, audio_file = await save_chat_entry(username, result, voice, True)
376
  return md_file, audio_file
377
 
 
408
  <div id="players">Players: 1</div>
409
  <div id="score">Score: 0</div>
410
  <div id="treasures">Treasures: 0</div>
411
+ <div id="timeout">Timeout: 60s</div>
412
  </div>
413
  <div id="chatBox"></div>
414
  <div id="controls">WASD/Arrows to move, Space to collect treasure</div>
 
446
  let treasures = [];
447
  const treasureMeshes = {{}};
448
  let xPos = 0, zPos = 0, moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
449
+ let score = 0, treasureCount = 0, lastActive = performance.now() / 1000;
450
 
451
  // Player initialization
452
  const playerGeometry = new THREE.BoxGeometry(2, 2, 2);
453
+ const playerMaterial = new THREE.MeshPhongMaterial({{ color: {next(c["color"] for c in CHARACTERS if c["name"] == st.session_state.username)} }});
454
  const playerMesh = new THREE.Mesh(playerGeometry, playerMaterial);
455
  playerMesh.position.set(xPos, 1, zPos);
456
  playerMesh.castShadow = true;
 
474
  treasureMeshes[t.id].position.set(t.x, 1, t.z);
475
  }}
476
  }});
 
477
  Object.keys(treasureMeshes).forEach(id => {{
478
  if (!treasureData.some(t => t.id === id)) {{
479
  scene.remove(treasureMeshes[id]);
 
492
  case 'ArrowDown': case 'KeyS': moveDown = true; break;
493
  case 'Space': collect = true; break;
494
  }}
495
+ lastActive = performance.now() / 1000;
496
  }});
497
  document.addEventListener('keyup', (event) => {{
498
  switch (event.code) {{
 
530
 
531
  camera.position.set(xPos, 50, zPos + 50);
532
  camera.lookAt(xPos, 0, zPos);
533
+ const timeout = 60 - (performance.now() / 1000 - lastActive);
534
+ document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, timeout.toFixed(0))}}s`;
535
  }}
536
 
537
  function updatePlayers(playerData) {{
 
602
  def main():
603
  st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
604
  st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: {st.session_state.score} ๐Ÿ†")
605
+ st.sidebar.write(f"๐Ÿ“œ {next(c['desc'] for c in CHARACTERS if c['name'] == st.session_state.username)}")
606
  st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
607
  st.sidebar.write(f"๐Ÿ… Score: {st.session_state.score}")
608
  st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
609
  st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
610
 
 
611
  st.session_state.update_interval = st.sidebar.slider("Refresh Interval (seconds)", 1, 10, 1, step=1)
612
+ if st.sidebar.button("Reset World"):
613
+ reset_game_state()
614
+ st.session_state.game_state_timestamp = time.time()
 
 
 
615
  st.rerun()
616
 
617
  left_col, right_col = st.columns([2, 1])
 
623
  message = st.text_input(f"๐Ÿ—จ๏ธ {st.session_state.username} says:", placeholder="Speak or type to chat! ๐ŸŒฒ")
624
  if st.button("๐ŸŒŸ Send & Chat ๐ŸŽค"):
625
  if message:
626
+ voice = next(c["voice"] for c in CHARACTERS if c["name"] == st.session_state.username)
627
  md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
628
  if audio_file:
629
  play_and_download_audio(audio_file)
630
+ st.session_state.last_activity = time.time()
631
  st.success(f"๐ŸŒ„ +10 points! New Score: {st.session_state.score}")
632
  mycomponent = components.declare_component("speech_component", path="./speech_component")
633
  val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
634
  if val and val != st.session_state.last_transcript:
635
  val_stripped = val.strip().replace('\n', ' ')
636
  if val_stripped:
637
+ voice = next(c["voice"] for c in CHARACTERS if c["name"] == st.session_state.username)
638
  st.session_state['speech_processed'] = True
639
  md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
640
  if audio_file:
641
  play_and_download_audio(audio_file)
642
+ st.session_state.last_activity = time.time()
643
  st.rerun()
644
 
645
  with right_col:
 
647
  prairie_map = folium.Map(location=[44.0, -103.0], zoom_start=8, tiles="CartoDB Positron")
648
  for loc, (lat, lon) in PRAIRIE_LOCATIONS.items():
649
  folium.Marker([lat, lon], popup=loc).add_to(prairie_map)
650
+ game_state = load_game_state(st.session_state.game_state_timestamp)
651
+ for player in game_state["players"].values():
652
+ folium.CircleMarker(
653
+ location=[44.0 + player["x"] * 0.01, -103.0 + player["z"] * 0.01], # Approximate mapping to lat/lon
654
+ radius=5,
655
+ color=f"#{player['color']:06x}",
656
+ fill=True,
657
+ fill_opacity=0.7,
658
+ popup=f"{player['username']} (Score: {player['score']}, Treasures: {player['treasures']})"
659
+ ).add_to(prairie_map)
660
  for client_id, player in st.session_state.prairie_players.items():
661
  folium.CircleMarker(
662
  location=player['location'],
 
673
  if st.button("Move"):
674
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:ANIMAL:{animal}", "quest"))
675
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:MOVE:{location}", "quest"))
676
+ st.session_state.last_activity = time.time()
677
  st.rerun()
678
 
679
  elapsed = time.time() - st.session_state.last_update
680
  remaining = max(0, st.session_state.update_interval - elapsed)
681
  st.sidebar.markdown(f"โณ Next Update in: {int(remaining)}s")
682
+
683
+ # Timeout and Auto-Refresh Logic
684
+ if time.time() - st.session_state.last_activity > st.session_state.timeout:
685
+ st.sidebar.warning("Timed out! Refreshing in 5 seconds...")
686
+ time.sleep(5)
687
+ st.session_state.game_state_timestamp = time.time()
688
+ st.rerun()
689
+ elif time.time() - st.session_state.last_update > st.session_state.auto_refresh:
690
+ st.session_state.game_state_timestamp = time.time()
691
+ st.rerun()
692
+ elif remaining <= 0:
693
  st.session_state.last_update = time.time()
694
+ st.session_state.game_state_timestamp = time.time()
695
  st.rerun()
696
 
697
  if not st.session_state.get('server_running', False):