awacke1 commited on
Commit
0029495
ยท
verified ยท
1 Parent(s): e29e27a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -109
app.py CHANGED
@@ -43,24 +43,24 @@ st.set_page_config(
43
  GAME_NAME = "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
44
  START_LOCATION = "Trailhead Camp โ›บ"
45
  EDGE_TTS_VOICES = [
46
- {"name": "Aria ๐ŸŒธ", "voice": "en-US-AriaNeural", "desc": "Elegant, creative storytelling", "color": 0xFF69B4},
47
- {"name": "Guy ๐ŸŒŸ", "voice": "en-US-GuyNeural", "desc": "Authoritative, versatile", "color": 0x00FF00},
48
- {"name": "Jenny ๐ŸŽถ", "voice": "en-US-JennyNeural", "desc": "Friendly, conversational", "color": 0xFFFF00},
49
- {"name": "Sonia ๐ŸŒบ", "voice": "en-GB-SoniaNeural", "desc": "Bold, confident", "color": 0x0000FF},
50
- {"name": "Ryan ๐Ÿ› ๏ธ", "voice": "en-GB-RyanNeural", "desc": "Approachable, casual", "color": 0x00FFFF},
51
- {"name": "Natasha ๐ŸŒŒ", "voice": "en-AU-NatashaNeural", "desc": "Sophisticated, mysterious", "color": 0x800080},
52
- {"name": "William ๐ŸŽป", "voice": "en-AU-WilliamNeural", "desc": "Classic, scholarly", "color": 0xFFA500},
53
- {"name": "Clara ๐ŸŒท", "voice": "en-CA-ClaraNeural", "desc": "Cheerful, empathetic", "color": 0xFF4500},
54
- {"name": "Liam ๐ŸŒŸ", "voice": "en-CA-LiamNeural", "desc": "Energetic, engaging", "color": 0xFFD700}
55
  ]
56
  FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
57
-
58
- # Prairie Simulator Locations
59
- PRAIRIE_LOCATIONS = {
60
- "Deadwood, SD": (44.3769, -103.7298),
61
- "Wind Cave National Park": (43.6047, -103.4798),
62
- "Wyoming Spring Creek": (41.6666, -106.6666)
63
- }
64
 
65
  # Directories and Files
66
  for d in ["chat_logs", "audio_logs"]:
@@ -89,6 +89,7 @@ def load_game_state(_timestamp):
89
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
90
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
91
  ],
 
92
  "history": []
93
  }
94
  with open(GAME_STATE_FILE, 'w') as f:
@@ -100,7 +101,7 @@ def update_game_state(state):
100
  state["timestamp"] = os.path.getmtime(GAME_STATE_FILE) if os.path.exists(GAME_STATE_FILE) else time.time()
101
  with open(GAME_STATE_FILE, 'w') as f:
102
  json.dump(state, f)
103
- load_game_state.clear() # Clear cache to force reload
104
  return load_game_state(time.time())
105
 
106
  def reset_game_state():
@@ -114,10 +115,50 @@ def reset_game_state():
114
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
115
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
116
  ],
 
117
  "history": []
118
  }
119
  return update_game_state(state)
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  # Helpers
122
  def format_timestamp(username=""):
123
  now = datetime.now(pytz.timezone('US/Central')).strftime("%Y%m%d_%H%M%S")
@@ -204,6 +245,17 @@ async def save_chat_entry(username, message, voice, is_markdown=False):
204
  game_state["players"][username]["treasures"] += 1
205
  game_state["players"][username]["last_active"] = time.time()
206
  game_state["history"].append(entry)
 
 
 
 
 
 
 
 
 
 
 
207
  update_game_state(game_state)
208
  return md_file, audio_file
209
 
@@ -233,27 +285,15 @@ def init_session_state():
233
  if k not in st.session_state:
234
  st.session_state[k] = v
235
  if st.session_state.username is None:
236
- saved_username = load_username()
237
- if saved_username and any(c["name"] == saved_username for c in EDGE_TTS_VOICES):
238
- st.session_state.username = saved_username
239
- st.session_state.tts_voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == saved_username)
240
- else:
241
- char = random.choice(EDGE_TTS_VOICES)
242
- st.session_state.username = char["name"]
243
- st.session_state.tts_voice = char["voice"]
244
- asyncio.run(save_chat_entry(st.session_state.username, "๐Ÿ—บ๏ธ Joins the Rocky Mountain Quest!", char["voice"]))
245
- save_username(st.session_state.username)
246
  game_state = load_game_state(st.session_state.game_state_timestamp)
247
  if st.session_state.username not in game_state["players"]:
248
  char = next(c for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
249
- game_state["players"][st.session_state.username] = {
250
- "x": random.uniform(-20, 20),
251
- "z": random.uniform(-40, 40),
252
- "color": char["color"],
253
- "score": 0,
254
- "treasures": 0,
255
- "last_active": time.time()
256
- }
257
  update_game_state(game_state)
258
 
259
  init_session_state()
@@ -280,14 +320,8 @@ async def websocket_handler(websocket, path):
280
  else:
281
  if username not in game_state["players"]:
282
  char = next(c for c in EDGE_TTS_VOICES if c["name"] == username)
283
- game_state["players"][username] = {
284
- "x": random.uniform(-20, 20),
285
- "z": random.uniform(-40, 40),
286
- "color": char["color"],
287
- "score": 0,
288
- "treasures": 0,
289
- "last_active": time.time()
290
- }
291
  update_game_state(game_state)
292
  st.session_state.players[client_id] = game_state["players"][username]
293
  await broadcast_message(f"System|{username} joins the quest!", room_id)
@@ -298,33 +332,11 @@ async def websocket_handler(websocket, path):
298
  sender, content = message.split('|', 1)
299
  voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == sender)
300
  game_state = load_game_state(st.session_state.game_state_timestamp)
301
- if content.startswith("MOVE:"):
302
- _, x, z = content.split(":")
303
- x, z = float(x), float(z)
304
- st.session_state.players[client_id]["x"] = x
305
- st.session_state.players[client_id]["z"] = z
306
- game_state["players"][sender]["x"] = x
307
- game_state["players"][sender]["z"] = z
308
- game_state["players"][sender]["last_active"] = time.time()
309
- update_game_state(game_state)
310
- elif content.startswith("SCORE:"):
311
- score = int(content.split(":")[1])
312
- st.session_state.players[client_id]["score"] = score
313
- game_state["players"][sender]["score"] = score
314
- game_state["players"][sender]["last_active"] = time.time()
315
- update_game_state(game_state)
316
- elif content.startswith("TREASURE:"):
317
- treasures = int(content.split(":")[1])
318
- st.session_state.players[client_id]["treasures"] = treasures
319
- game_state["players"][sender]["treasures"] = treasures
320
- game_state["players"][sender]["last_active"] = time.time()
321
- for i, t in enumerate(game_state["treasures"]):
322
- if abs(st.session_state.players[client_id]["x"] - t["x"]) < 2 and \
323
- abs(st.session_state.players[client_id]["z"] - t["z"]) < 2:
324
- game_state["treasures"].pop(i)
325
- break
326
- update_game_state(game_state)
327
- elif content.startswith("PRAIRIE:"):
328
  action, value = content.split(":", 1)
329
  if action == "ANIMAL":
330
  st.session_state.prairie_players[client_id]["animal"] = value
@@ -336,6 +348,7 @@ async def websocket_handler(websocket, path):
336
  else:
337
  await save_chat_entry(sender, content, voice)
338
  await perform_arxiv_search(content, sender)
 
339
  except websockets.ConnectionClosed:
340
  await broadcast_message(f"System|{username} leaves the quest!", room_id)
341
  if client_id in st.session_state.players:
@@ -355,6 +368,8 @@ async def periodic_update():
355
  for player in inactive_players:
356
  del game_state["players"][player]
357
  await broadcast_message(f"System|{player} timed out after 60 seconds!", "quest")
 
 
358
  update_game_state(game_state)
359
 
360
  player_list = ", ".join([p for p in game_state["players"].keys()]) or "No adventurers yet!"
@@ -420,7 +435,7 @@ def display_file_history_in_sidebar():
420
  if not chat_files:
421
  st.sidebar.write("No chat audio files found.")
422
  return
423
- for audio_file in chat_files[:10]: # Limit to last 10 for brevity
424
  with st.sidebar.expander(os.path.basename(audio_file)):
425
  st.write(f"**Said:** {os.path.splitext(os.path.basename(audio_file))[0].split('_')[0]}")
426
  play_and_download_audio(audio_file)
@@ -447,31 +462,41 @@ rocky_map_html = f"""
447
  body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
448
  #gameContainer {{ width: 800px; height: 600px; position: relative; }}
449
  canvas {{ width: 100%; height: 100%; display: block; }}
450
- #ui {{
451
- position: absolute; top: 10px; left: 10px; color: white;
452
- background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 5px;
 
 
 
 
 
 
453
  }}
454
- #chatBox {{
455
- position: absolute; bottom: 60px; left: 10px; width: 300px; height: 150px;
456
- background: rgba(0, 0, 0, 0.7); color: white; padding: 10px;
457
- border-radius: 5px; overflow-y: auto;
458
  }}
459
- #controls {{
460
- position: absolute; bottom: 10px; left: 10px; color: white;
461
- background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 5px;
 
462
  }}
463
  </style>
464
  </head>
465
  <body>
466
  <div id="gameContainer">
467
- <div id="ui">
468
- <div id="players">Players: 1</div>
469
  <div id="score">Score: 0</div>
470
  <div id="treasures">Treasures: 0</div>
471
  <div id="timeout">Timeout: 60s</div>
472
  </div>
 
473
  <div id="chatBox"></div>
474
- <div id="controls">WASD/Arrows to move, Space to collect treasure</div>
 
 
 
475
  </div>
476
 
477
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
@@ -505,12 +530,25 @@ rocky_map_html = f"""
505
  const playerMeshes = {{}};
506
  let treasures = [];
507
  const treasureMeshes = {{}};
 
 
508
  let xPos = 0, zPos = 0, moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
509
  let score = 0, treasureCount = 0, lastActive = performance.now() / 1000;
510
 
511
- const playerGeometry = new THREE.BoxGeometry(2, 2, 2);
 
 
 
 
 
 
 
 
 
 
 
512
  const playerMaterial = new THREE.MeshPhongMaterial({{ color: {next(c["color"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)} }});
513
- const playerMesh = new THREE.Mesh(playerGeometry, playerMaterial);
514
  playerMesh.position.set(xPos, 1, zPos);
515
  playerMesh.castShadow = true;
516
  scene.add(playerMesh);
@@ -541,6 +579,21 @@ rocky_map_html = f"""
541
  }});
542
  }}
543
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
  document.addEventListener('keydown', (event) => {{
545
  switch (event.code) {{
546
  case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
@@ -589,12 +642,16 @@ rocky_map_html = f"""
589
  camera.lookAt(xPos, 0, zPos);
590
  const timeout = 60 - (performance.now() / 1000 - lastActive);
591
  document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, timeout.toFixed(0))}}s`;
 
 
592
  }}
593
 
594
  function updatePlayers(playerData) {{
595
  playerData.forEach(player => {{
596
  if (!playerMeshes[player.username]) {{
597
- const mesh = new THREE.Mesh(playerGeometry, new THREE.MeshPhongMaterial({{ color: player.color }}));
 
 
598
  mesh.castShadow = true;
599
  scene.add(mesh);
600
  playerMeshes[player.username] = mesh;
@@ -611,8 +668,10 @@ rocky_map_html = f"""
611
  }}
612
  }});
613
  document.getElementById('players').textContent = `Players: ${{Object.keys(playerMeshes).length}}`;
614
- document.getElementById('score').textContent = `Score: ${{score}}`;
615
- document.getElementById('treasures').textContent = `Treasures: ${{treasureCount}}`;
 
 
616
  }}
617
 
618
  ws.onmessage = function(event) {{
@@ -629,6 +688,7 @@ rocky_map_html = f"""
629
  const gameState = JSON.parse(data.split('GAME_STATE:')[1]);
630
  updatePlayers(gameState.players);
631
  updateTreasures(gameState.treasures);
 
632
  }} else if (!data.startsWith('PRAIRIE_UPDATE:')) {{
633
  const [sender, message] = data.split('|');
634
  const chatBox = document.getElementById('chatBox');
@@ -646,6 +706,7 @@ rocky_map_html = f"""
646
 
647
  updatePlayer(delta);
648
  treasures.forEach(t => t.rotation.y += delta);
 
649
  renderer.render(scene, camera);
650
  }}
651
 
@@ -657,37 +718,49 @@ rocky_map_html = f"""
657
 
658
  # Main Game Loop
659
  def main():
 
 
 
 
 
 
 
 
 
 
 
 
660
  update_marquee_settings_ui()
661
  st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
662
- st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: {st.session_state.score} ๐Ÿ†")
663
  st.sidebar.write(f"๐Ÿ“œ {next(c['desc'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}")
664
  st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
665
- st.sidebar.write(f"๐Ÿ… Score: {st.session_state.score}")
666
  st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
667
  st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
668
 
669
  st.session_state.update_interval = st.sidebar.slider("Refresh Interval (seconds)", 1, 10, 1, step=1)
670
- if st.sidebar.button("Reset World"):
671
  reset_game_state()
672
  st.session_state.game_state_timestamp = time.time()
673
  st.rerun()
674
 
 
 
 
 
 
 
 
 
 
675
  left_col, right_col = st.columns([2, 1])
676
 
677
  with left_col:
678
  components.html(rocky_map_html, width=800, height=600)
679
  chat_content = asyncio.run(load_chat())
680
  st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
681
- message = st.text_input(f"๐Ÿ—จ๏ธ {st.session_state.username} says:", placeholder="Speak or type to chat! ๐ŸŒฒ")
682
- if st.button("๐ŸŒŸ Send & Chat ๐ŸŽค"):
683
- if message:
684
- voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
685
- md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
686
- if audio_file:
687
- play_and_download_audio(audio_file)
688
- st.session_state.last_activity = time.time()
689
- st.success(f"๐ŸŒ„ +10 points! New Score: {st.session_state.score}")
690
- uploaded_file = st.file_uploader("Upload a File", type=["txt", "md", "mp3"])
691
  if uploaded_file:
692
  with open(uploaded_file.name, "wb") as f:
693
  f.write(uploaded_file.getbuffer())
@@ -697,7 +770,7 @@ def main():
697
  game_state["history"].append(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {st.session_state.username}: Uploaded {uploaded_file.name}")
698
  update_game_state(game_state)
699
  st.session_state.last_activity = time.time()
700
- st.success(f"File uploaded! +20 points! New Score: {st.session_state.score}")
701
  mycomponent = components.declare_component("speech_component", path="./speech_component")
702
  val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
703
  if val and val != st.session_state.last_transcript:
@@ -717,14 +790,14 @@ def main():
717
  for loc, (lat, lon) in PRAIRIE_LOCATIONS.items():
718
  folium.Marker([lat, lon], popup=loc).add_to(prairie_map)
719
  game_state = load_game_state(st.session_state.game_state_timestamp)
720
- for username, player in game_state["players"].items(): # Use .items() to get username and data
721
  folium.CircleMarker(
722
  location=[44.0 + player["x"] * 0.01, -103.0 + player["z"] * 0.01],
723
  radius=5,
724
  color=f"#{player['color']:06x}",
725
  fill=True,
726
  fill_opacity=0.7,
727
- popup=f"{username} (Score: {player['score']}, Treasures: {player['treasures']})"
728
  ).add_to(prairie_map)
729
  for client_id, player in st.session_state.prairie_players.items():
730
  folium.CircleMarker(
@@ -737,9 +810,9 @@ def main():
737
  ).add_to(prairie_map)
738
  folium_static(prairie_map, width=600, height=400)
739
 
740
- animal = st.selectbox("Choose Animal", ["prairie_dog", "deer", "sheep", "groundhog"])
741
- location = st.selectbox("Move to", list(PRAIRIE_LOCATIONS.keys()))
742
- if st.button("Move"):
743
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:ANIMAL:{animal}", "quest"))
744
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:MOVE:{location}", "quest"))
745
  st.session_state.last_activity = time.time()
@@ -750,7 +823,7 @@ def main():
750
  st.sidebar.markdown(f"โณ Next Update in: {int(remaining)}s")
751
 
752
  if time.time() - st.session_state.last_activity > st.session_state.timeout:
753
- st.sidebar.warning("Timed out! Refreshing in 5 seconds...")
754
  time.sleep(5)
755
  st.session_state.game_state_timestamp = time.time()
756
  st.rerun()
 
43
  GAME_NAME = "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
44
  START_LOCATION = "Trailhead Camp โ›บ"
45
  EDGE_TTS_VOICES = [
46
+ {"name": "Aria ๐ŸŒธ", "voice": "en-US-AriaNeural", "desc": "Elegant, creative storytelling", "color": 0xFF69B4, "shape": "sphere"},
47
+ {"name": "Guy ๐ŸŒŸ", "voice": "en-US-GuyNeural", "desc": "Authoritative, versatile", "color": 0x00FF00, "shape": "cube"},
48
+ {"name": "Jenny ๐ŸŽถ", "voice": "en-US-JennyNeural", "desc": "Friendly, conversational", "color": 0xFFFF00, "shape": "cylinder"},
49
+ {"name": "Sonia ๐ŸŒบ", "voice": "en-GB-SoniaNeural", "desc": "Bold, confident", "color": 0x0000FF, "shape": "cone"},
50
+ {"name": "Ryan ๐Ÿ› ๏ธ", "voice": "en-GB-RyanNeural", "desc": "Approachable, casual", "color": 0x00FFFF, "shape": "torus"},
51
+ {"name": "Natasha ๐ŸŒŒ", "voice": "en-AU-NatashaNeural", "desc": "Sophisticated, mysterious", "color": 0x800080, "shape": "dodecahedron"},
52
+ {"name": "William ๐ŸŽป", "voice": "en-AU-WilliamNeural", "desc": "Classic, scholarly", "color": 0xFFA500, "shape": "octahedron"},
53
+ {"name": "Clara ๐ŸŒท", "voice": "en-CA-ClaraNeural", "desc": "Cheerful, empathetic", "color": 0xFF4500, "shape": "tetrahedron"},
54
+ {"name": "Liam ๐ŸŒŸ", "voice": "en-CA-LiamNeural", "desc": "Energetic, engaging", "color": 0xFFD700, "shape": "icosahedron"}
55
  ]
56
  FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
57
+ WORLD_OBJECTS = [
58
+ {"type": "๐ŸŒฟ Plants", "emoji": "๐ŸŒฑ๐ŸŒฟ๐ŸŒพ", "color": 0x228B22, "shape": "cylinder", "count": 10},
59
+ {"type": "๐Ÿฆ Animal Flocks", "emoji": "๐Ÿ•Š๏ธ๐Ÿฆ๐Ÿค", "color": 0x87CEEB, "shape": "sphere", "count": 5},
60
+ {"type": "๐Ÿ”๏ธ Mountains", "emoji": "๐Ÿ”๏ธโ›ฐ๏ธ", "color": 0x808080, "shape": "cone", "count": 3},
61
+ {"type": "๐ŸŒณ Trees", "emoji": "๐ŸŒฒ๐ŸŒณ๐ŸŒด", "color": 0x006400, "shape": "cylinder", "count": 8},
62
+ {"type": "๐Ÿพ Animals", "emoji": "๐Ÿป๐Ÿบ๐ŸฆŒ", "color": 0x8B4513, "shape": "cube", "count": 6}
63
+ ]
64
 
65
  # Directories and Files
66
  for d in ["chat_logs", "audio_logs"]:
 
89
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
90
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
91
  ],
92
+ "world_objects": [],
93
  "history": []
94
  }
95
  with open(GAME_STATE_FILE, 'w') as f:
 
101
  state["timestamp"] = os.path.getmtime(GAME_STATE_FILE) if os.path.exists(GAME_STATE_FILE) else time.time()
102
  with open(GAME_STATE_FILE, 'w') as f:
103
  json.dump(state, f)
104
+ load_game_state.clear()
105
  return load_game_state(time.time())
106
 
107
  def reset_game_state():
 
115
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
116
  {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
117
  ],
118
+ "world_objects": [],
119
  "history": []
120
  }
121
  return update_game_state(state)
122
 
123
+ # Agent Class for Players
124
+ class PlayerAgent:
125
+ def __init__(self, username, char_data):
126
+ self.username = username
127
+ self.x = random.uniform(-20, 20)
128
+ self.z = random.uniform(-40, 40)
129
+ self.color = char_data["color"]
130
+ self.shape = char_data["shape"]
131
+ self.score = 0
132
+ self.treasures = 0
133
+ self.last_active = time.time()
134
+ self.voice = char_data["voice"]
135
+
136
+ def to_dict(self):
137
+ return {
138
+ "username": self.username,
139
+ "x": self.x,
140
+ "z": self.z,
141
+ "color": self.color,
142
+ "shape": self.shape,
143
+ "score": self.score,
144
+ "treasures": self.treasures,
145
+ "last_active": self.last_active
146
+ }
147
+
148
+ def update_from_message(self, message):
149
+ if '|' in message:
150
+ _, content = message.split('|', 1)
151
+ if content.startswith("MOVE:"):
152
+ _, x, z = content.split(":")
153
+ self.x, self.z = float(x), float(z)
154
+ self.last_active = time.time()
155
+ elif content.startswith("SCORE:"):
156
+ self.score = int(content.split(":")[1])
157
+ self.last_active = time.time()
158
+ elif content.startswith("TREASURE:"):
159
+ self.treasures = int(content.split(":")[1])
160
+ self.last_active = time.time()
161
+
162
  # Helpers
163
  def format_timestamp(username=""):
164
  now = datetime.now(pytz.timezone('US/Central')).strftime("%Y%m%d_%H%M%S")
 
245
  game_state["players"][username]["treasures"] += 1
246
  game_state["players"][username]["last_active"] = time.time()
247
  game_state["history"].append(entry)
248
+ if message.lower() in ["plants", "animal flocks", "mountains", "trees", "animals"]:
249
+ obj_type = next(o for o in WORLD_OBJECTS if message.lower() in o["type"].lower())
250
+ for _ in range(obj_type["count"]):
251
+ game_state["world_objects"].append({
252
+ "type": obj_type["type"],
253
+ "emoji": random.choice(obj_type["emoji"].split()),
254
+ "x": random.uniform(-40, 40),
255
+ "z": random.uniform(-40, 40),
256
+ "color": obj_type["color"],
257
+ "shape": obj_type["shape"]
258
+ })
259
  update_game_state(game_state)
260
  return md_file, audio_file
261
 
 
285
  if k not in st.session_state:
286
  st.session_state[k] = v
287
  if st.session_state.username is None:
288
+ char = random.choice(EDGE_TTS_VOICES)
289
+ st.session_state.username = char["name"]
290
+ st.session_state.tts_voice = char["voice"]
291
+ asyncio.run(save_chat_entry(st.session_state.username, f"๐Ÿ—บ๏ธ Welcome to the Rocky Mountain Quest, {st.session_state.username}!", char["voice"]))
 
 
 
 
 
 
292
  game_state = load_game_state(st.session_state.game_state_timestamp)
293
  if st.session_state.username not in game_state["players"]:
294
  char = next(c for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
295
+ agent = PlayerAgent(st.session_state.username, char)
296
+ game_state["players"][st.session_state.username] = agent.to_dict()
 
 
 
 
 
 
297
  update_game_state(game_state)
298
 
299
  init_session_state()
 
320
  else:
321
  if username not in game_state["players"]:
322
  char = next(c for c in EDGE_TTS_VOICES if c["name"] == username)
323
+ agent = PlayerAgent(username, char)
324
+ game_state["players"][username] = agent.to_dict()
 
 
 
 
 
 
325
  update_game_state(game_state)
326
  st.session_state.players[client_id] = game_state["players"][username]
327
  await broadcast_message(f"System|{username} joins the quest!", room_id)
 
332
  sender, content = message.split('|', 1)
333
  voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == sender)
334
  game_state = load_game_state(st.session_state.game_state_timestamp)
335
+ agent = PlayerAgent(sender, next(c for c in EDGE_TTS_VOICES if c["name"] == sender))
336
+ agent.update_from_message(f"{sender}|{content}")
337
+ if sender in game_state["players"]:
338
+ game_state["players"][sender] = agent.to_dict()
339
+ if content.startswith("PRAIRIE:"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  action, value = content.split(":", 1)
341
  if action == "ANIMAL":
342
  st.session_state.prairie_players[client_id]["animal"] = value
 
348
  else:
349
  await save_chat_entry(sender, content, voice)
350
  await perform_arxiv_search(content, sender)
351
+ update_game_state(game_state)
352
  except websockets.ConnectionClosed:
353
  await broadcast_message(f"System|{username} leaves the quest!", room_id)
354
  if client_id in st.session_state.players:
 
368
  for player in inactive_players:
369
  del game_state["players"][player]
370
  await broadcast_message(f"System|{player} timed out after 60 seconds!", "quest")
371
+ for username in game_state["players"]:
372
+ game_state["players"][username]["score"] += int((current_time - game_state["players"][username]["last_active"]) * 60) # $1 per second
373
  update_game_state(game_state)
374
 
375
  player_list = ", ".join([p for p in game_state["players"].keys()]) or "No adventurers yet!"
 
435
  if not chat_files:
436
  st.sidebar.write("No chat audio files found.")
437
  return
438
+ for audio_file in chat_files[:10]:
439
  with st.sidebar.expander(os.path.basename(audio_file)):
440
  st.write(f"**Said:** {os.path.splitext(os.path.basename(audio_file))[0].split('_')[0]}")
441
  play_and_download_audio(audio_file)
 
462
  body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
463
  #gameContainer {{ width: 800px; height: 600px; position: relative; }}
464
  canvas {{ width: 100%; height: 100%; display: block; }}
465
+ .ui-container {{
466
+ position: absolute; top: 10px; left: 10px; color: white;
467
+ background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
468
+ user-select: none;
469
+ }}
470
+ .leaderboard {{
471
+ position: absolute; top: 10px; left: 50%; transform: translateX(-50%); color: white;
472
+ background-color: rgba(0, 0, 0, 0.7); padding: 10px; border-radius: 5px;
473
+ text-align: center;
474
  }}
475
+ .controls {{
476
+ position: absolute; bottom: 10px; left: 10px; color: white;
477
+ background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
 
478
  }}
479
+ #chatBox {{
480
+ position: absolute; bottom: 60px; left: 10px; width: 300px; height: 150px;
481
+ background: rgba(0, 0, 0, 0.7); color: white; padding: 10px;
482
+ border-radius: 5px; overflow-y: auto;
483
  }}
484
  </style>
485
  </head>
486
  <body>
487
  <div id="gameContainer">
488
+ <div class="ui-container">
489
+ <h2>{GAME_NAME}</h2>
490
  <div id="score">Score: 0</div>
491
  <div id="treasures">Treasures: 0</div>
492
  <div id="timeout">Timeout: 60s</div>
493
  </div>
494
+ <div class="leaderboard" id="leaderboard">Leaderboard</div>
495
  <div id="chatBox"></div>
496
+ <div class="controls">
497
+ <p>Controls: WASD/Arrows to move, Space to collect treasure</p>
498
+ <p>Chat to add world features!</p>
499
+ </div>
500
  </div>
501
 
502
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
 
530
  const playerMeshes = {{}};
531
  let treasures = [];
532
  const treasureMeshes = {{}};
533
+ let worldObjects = [];
534
+ const worldObjectMeshes = {{}};
535
  let xPos = 0, zPos = 0, moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
536
  let score = 0, treasureCount = 0, lastActive = performance.now() / 1000;
537
 
538
+ const shapeGeometries = {{
539
+ "sphere": new THREE.SphereGeometry(1, 16, 16),
540
+ "cube": new THREE.BoxGeometry(2, 2, 2),
541
+ "cylinder": new THREE.CylinderGeometry(1, 1, 2, 16),
542
+ "cone": new THREE.ConeGeometry(1, 2, 16),
543
+ "torus": new THREE.TorusGeometry(1, 0.4, 16, 100),
544
+ "dodecahedron": new THREE.DodecahedronGeometry(1),
545
+ "octahedron": new THREE.OctahedronGeometry(1),
546
+ "tetrahedron": new THREE.TetrahedronGeometry(1),
547
+ "icosahedron": new THREE.IcosahedronGeometry(1)
548
+ }};
549
+
550
  const playerMaterial = new THREE.MeshPhongMaterial({{ color: {next(c["color"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)} }});
551
+ const playerMesh = new THREE.Mesh(shapeGeometries["{next(c['shape'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}"], playerMaterial);
552
  playerMesh.position.set(xPos, 1, zPos);
553
  playerMesh.castShadow = true;
554
  scene.add(playerMesh);
 
579
  }});
580
  }}
581
 
582
+ function updateWorldObjects(objectData) {{
583
+ objectData.forEach(obj => {{
584
+ if (!worldObjectMeshes[obj.type + obj.x + obj.z]) {{
585
+ const geometry = shapeGeometries[obj.shape] || new THREE.BoxGeometry(2, 2, 2);
586
+ const material = new THREE.MeshPhongMaterial({{ color: obj.color }});
587
+ const objMesh = new THREE.Mesh(geometry, material);
588
+ objMesh.position.set(obj.x, 1, obj.z);
589
+ objMesh.castShadow = true;
590
+ worldObjectMeshes[obj.type + obj.x + obj.z] = objMesh;
591
+ worldObjects.push(objMesh);
592
+ scene.add(objMesh);
593
+ }}
594
+ }});
595
+ }}
596
+
597
  document.addEventListener('keydown', (event) => {{
598
  switch (event.code) {{
599
  case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
 
642
  camera.lookAt(xPos, 0, zPos);
643
  const timeout = 60 - (performance.now() / 1000 - lastActive);
644
  document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, timeout.toFixed(0))}}s`;
645
+ document.getElementById('score').textContent = `Score: $${{score}}`;
646
+ document.getElementById('treasures').textContent = `Treasures: ${{treasureCount}}`;
647
  }}
648
 
649
  function updatePlayers(playerData) {{
650
  playerData.forEach(player => {{
651
  if (!playerMeshes[player.username]) {{
652
+ const geometry = shapeGeometries[player.shape] || new THREE.BoxGeometry(2, 2, 2);
653
+ const material = new THREE.MeshPhongMaterial({{ color: player.color }});
654
+ const mesh = new THREE.Mesh(geometry, material);
655
  mesh.castShadow = true;
656
  scene.add(mesh);
657
  playerMeshes[player.username] = mesh;
 
668
  }}
669
  }});
670
  document.getElementById('players').textContent = `Players: ${{Object.keys(playerMeshes).length}}`;
671
+ const leaderboard = playerData.sort((a, b) => b.score - a.score)
672
+ .map(p => `${{p.username}}: $${{p.score}}`)
673
+ .join('<br>');
674
+ document.getElementById('leaderboard').innerHTML = `Leaderboard<br>${{leaderboard}}`;
675
  }}
676
 
677
  ws.onmessage = function(event) {{
 
688
  const gameState = JSON.parse(data.split('GAME_STATE:')[1]);
689
  updatePlayers(gameState.players);
690
  updateTreasures(gameState.treasures);
691
+ updateWorldObjects(gameState.world_objects);
692
  }} else if (!data.startsWith('PRAIRIE_UPDATE:')) {{
693
  const [sender, message] = data.split('|');
694
  const chatBox = document.getElementById('chatBox');
 
706
 
707
  updatePlayer(delta);
708
  treasures.forEach(t => t.rotation.y += delta);
709
+ worldObjects.forEach(o => o.rotation.y += delta * 0.5);
710
  renderer.render(scene, camera);
711
  }}
712
 
 
718
 
719
  # Main Game Loop
720
  def main():
721
+ # Top Center Chat and Character Display
722
+ st.markdown(f"<h2 style='text-align: center;'>Welcome, {st.session_state.username}!</h2>", unsafe_allow_html=True)
723
+ message = st.text_input(f"๐Ÿ—จ๏ธ Chat as {st.session_state.username}:", placeholder="Type to chat or add world features! ๐ŸŒฒ", key="chat_input")
724
+ if st.button("๐ŸŒŸ Send"):
725
+ if message:
726
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
727
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
728
+ if audio_file:
729
+ play_and_download_audio(audio_file)
730
+ st.session_state.last_activity = time.time()
731
+ st.success(f"๐ŸŒ„ +10 points! New Score: {st.session_state.score}")
732
+
733
  update_marquee_settings_ui()
734
  st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
735
+ st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: ${st.session_state.score} ๐Ÿ†")
736
  st.sidebar.write(f"๐Ÿ“œ {next(c['desc'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}")
737
  st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
738
+ st.sidebar.write(f"๐Ÿ… Score: ${st.session_state.score}")
739
  st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
740
  st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
741
 
742
  st.session_state.update_interval = st.sidebar.slider("Refresh Interval (seconds)", 1, 10, 1, step=1)
743
+ if st.sidebar.button("Reset World ๐ŸŒ"):
744
  reset_game_state()
745
  st.session_state.game_state_timestamp = time.time()
746
  st.rerun()
747
 
748
+ # Demo Buttons
749
+ st.sidebar.markdown("### ๐ŸŒŸ World Additions")
750
+ if st.sidebar.button("๐ŸŒฟ Add Plants"):
751
+ asyncio.run(save_chat_entry(st.session_state.username, "plants", st.session_state.tts_voice))
752
+ if st.sidebar.button("๐Ÿฆ Add Animal Flocks"):
753
+ asyncio.run(save_chat_entry(st.session_state.username, "animal flocks", st.session_state.tts_voice))
754
+ if st.sidebar.button("๐Ÿ”๏ธ Add Mountains"):
755
+ asyncio.run(save_chat_entry(st.session_state.username, "mountains", st.session_state.tts_voice))
756
+
757
  left_col, right_col = st.columns([2, 1])
758
 
759
  with left_col:
760
  components.html(rocky_map_html, width=800, height=600)
761
  chat_content = asyncio.run(load_chat())
762
  st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
763
+ uploaded_file = st.file_uploader("Upload a File ๐Ÿ“ค", type=["txt", "md", "mp3"])
 
 
 
 
 
 
 
 
 
764
  if uploaded_file:
765
  with open(uploaded_file.name, "wb") as f:
766
  f.write(uploaded_file.getbuffer())
 
770
  game_state["history"].append(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {st.session_state.username}: Uploaded {uploaded_file.name}")
771
  update_game_state(game_state)
772
  st.session_state.last_activity = time.time()
773
+ st.success(f"File uploaded! +20 points! New Score: ${st.session_state.score}")
774
  mycomponent = components.declare_component("speech_component", path="./speech_component")
775
  val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
776
  if val and val != st.session_state.last_transcript:
 
790
  for loc, (lat, lon) in PRAIRIE_LOCATIONS.items():
791
  folium.Marker([lat, lon], popup=loc).add_to(prairie_map)
792
  game_state = load_game_state(st.session_state.game_state_timestamp)
793
+ for username, player in game_state["players"].items():
794
  folium.CircleMarker(
795
  location=[44.0 + player["x"] * 0.01, -103.0 + player["z"] * 0.01],
796
  radius=5,
797
  color=f"#{player['color']:06x}",
798
  fill=True,
799
  fill_opacity=0.7,
800
+ popup=f"{username} (Score: ${player['score']}, Treasures: {player['treasures']})"
801
  ).add_to(prairie_map)
802
  for client_id, player in st.session_state.prairie_players.items():
803
  folium.CircleMarker(
 
810
  ).add_to(prairie_map)
811
  folium_static(prairie_map, width=600, height=400)
812
 
813
+ animal = st.selectbox("Choose Animal ๐Ÿพ", ["prairie_dog", "deer", "sheep", "groundhog"])
814
+ location = st.selectbox("Move to ๐Ÿ“", list(PRAIRIE_LOCATIONS.keys()))
815
+ if st.button("Move ๐Ÿšถ"):
816
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:ANIMAL:{animal}", "quest"))
817
  asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:MOVE:{location}", "quest"))
818
  st.session_state.last_activity = time.time()
 
823
  st.sidebar.markdown(f"โณ Next Update in: {int(remaining)}s")
824
 
825
  if time.time() - st.session_state.last_activity > st.session_state.timeout:
826
+ st.sidebar.warning("Timed out! Refreshing in 5 seconds... โฐ")
827
  time.sleep(5)
828
  st.session_state.game_state_timestamp = time.time()
829
  st.rerun()