awacke1 commited on
Commit
ca1ef1c
·
verified ·
1 Parent(s): 1e57548

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +754 -0
app.py ADDED
@@ -0,0 +1,754 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import streamlit.components.v1 as components
3
+ import asyncio
4
+ import websockets
5
+ import uuid
6
+ import os
7
+ import random
8
+ import time
9
+ import hashlib
10
+ from datetime import datetime
11
+ import pytz
12
+ import nest_asyncio
13
+ import edge_tts
14
+ from audio_recorder_streamlit import audio_recorder
15
+
16
+ # Patch asyncio for nesting
17
+ nest_asyncio.apply()
18
+
19
+ # Page Config
20
+ st.set_page_config(page_title="Galaxian Snake 3D Multiplayer", layout="wide")
21
+
22
+ st.title("Galaxian Snake 3D Multiplayer")
23
+ st.write("Navigate a 3D city, grow your snake, shoot enemies, and chat!")
24
+
25
+ # Sliders for container size
26
+ max_width = min(1200, st.session_state.get('window_width', 1200))
27
+ max_height = min(1600, st.session_state.get('window_height', 1600))
28
+
29
+ col1, col2 = st.columns(2)
30
+ with col1:
31
+ container_width = st.slider("Container Width (px)", 300, max_width, 768, step=50)
32
+ with col2:
33
+ container_height = st.slider("Container Height (px)", 400, max_height, 1024, step=50)
34
+
35
+ # Session State Initialization
36
+ def init_session_state():
37
+ defaults = {
38
+ 'server_running': False, 'active_connections': {},
39
+ 'chat_history': [], 'last_chat_update': 0, 'username': None,
40
+ 'tts_voice': "en-US-AriaNeural", 'audio_cache': {}
41
+ }
42
+ for k, v in defaults.items():
43
+ if k not in st.session_state:
44
+ st.session_state[k] = v
45
+
46
+ init_session_state()
47
+
48
+ # Usernames and Voices
49
+ FUN_USERNAMES = {
50
+ "CosmicJester 🌌": "en-US-AriaNeural",
51
+ "PixelPanda 🐼": "en-US-JennyNeural",
52
+ "QuantumQuack 🦆": "en-GB-SoniaNeural",
53
+ "StellarSquirrel 🐿️": "en-AU-NatashaNeural",
54
+ "GizmoGuru ⚙️": "en-CA-ClaraNeural",
55
+ "NebulaNinja 🌠": "en-US-GuyNeural",
56
+ }
57
+
58
+ if not st.session_state.username:
59
+ st.session_state.username = random.choice(list(FUN_USERNAMES.keys()))
60
+ st.session_state.tts_voice = FUN_USERNAMES[st.session_state.username]
61
+
62
+ # Chat File
63
+ CHAT_FILE = "chat_logs/global_chat.md"
64
+ os.makedirs("chat_logs", exist_ok=True)
65
+ if not os.path.exists(CHAT_FILE):
66
+ with open(CHAT_FILE, 'a') as f:
67
+ f.write("# Multiplayer Snake Chat\n\nWelcome to the cosmic city! 🎤\n")
68
+
69
+ # Audio Processing
70
+ async def async_edge_tts_generate(text, voice, username):
71
+ cache_key = f"{text[:100]}_{voice}"
72
+ if cache_key in st.session_state['audio_cache']:
73
+ return st.session_state['audio_cache'][cache_key]
74
+ filename = f"audio_logs/{datetime.now().strftime('%Y%m%d_%H%M%S')}-by-{username}-{hashlib.md5(text.encode()).hexdigest()[:8]}.mp3"
75
+ os.makedirs("audio_logs", exist_ok=True)
76
+ communicate = edge_tts.Communicate(text, voice)
77
+ await communicate.save(filename)
78
+ if os.path.exists(filename) and os.path.getsize(filename) > 0:
79
+ st.session_state['audio_cache'][cache_key] = filename
80
+ return filename
81
+ return None
82
+
83
+ def play_and_download_audio(file_path):
84
+ if file_path and os.path.exists(file_path):
85
+ with open(file_path, "rb") as f:
86
+ audio_bytes = f.read()
87
+ st.audio(audio_bytes, format="audio/mp3")
88
+ b64 = base64.b64encode(audio_bytes).decode()
89
+ st.markdown(f'<a href="data:audio/mpeg;base64,{b64}" download="{os.path.basename(file_path)}">🎵 Download {os.path.basename(file_path)}</a>', unsafe_allow_html=True)
90
+
91
+ # WebSocket Handling
92
+ async def websocket_handler(websocket, path):
93
+ client_id = str(uuid.uuid4())
94
+ room_id = "snake_chat"
95
+ if room_id not in st.session_state.active_connections:
96
+ st.session_state.active_connections[room_id] = {}
97
+ st.session_state.active_connections[room_id][client_id] = websocket
98
+ username = st.session_state.username
99
+ await broadcast_message(f"System|{username} has joined the game!", room_id)
100
+ try:
101
+ async for message in websocket:
102
+ if '|' in message:
103
+ sender, content = message.split('|', 1)
104
+ await save_chat_entry(sender, content)
105
+ else:
106
+ await websocket.send("ERROR|Message format: username|content")
107
+ except websockets.ConnectionClosed:
108
+ await broadcast_message(f"System|{username} has left the game!", room_id)
109
+ finally:
110
+ if room_id in st.session_state.active_connections and client_id in st.session_state.active_connections[room_id]:
111
+ del st.session_state.active_connections[room_id][client_id]
112
+
113
+ async def broadcast_message(message, room_id):
114
+ if room_id in st.session_state.active_connections:
115
+ disconnected = []
116
+ for client_id, ws in st.session_state.active_connections[room_id].items():
117
+ try:
118
+ await ws.send(message)
119
+ except websockets.ConnectionClosed:
120
+ disconnected.append(client_id)
121
+ for client_id in disconnected:
122
+ if client_id in st.session_state.active_connections[room_id]:
123
+ del st.session_state.active_connections[room_id][client_id]
124
+
125
+ async def start_websocket_server():
126
+ if not st.session_state.get('server_running', False):
127
+ server = await websockets.serve(websocket_handler, '0.0.0.0', 8765)
128
+ st.session_state['server_running'] = True
129
+ st.session_state['server'] = server
130
+ await asyncio.Future()
131
+
132
+ # Chat Functions
133
+ async def save_chat_entry(username, message):
134
+ central = pytz.timezone('US/Central')
135
+ timestamp = datetime.now(central).strftime("%Y-%m-%d %H:%M:%S")
136
+ entry = f"[{timestamp}] {username}: {message}"
137
+ with open(CHAT_FILE, 'a') as f:
138
+ f.write(f"{entry}\n")
139
+ audio_file = await async_edge_tts_generate(message, FUN_USERNAMES.get(username, "en-US-AriaNeural"), username)
140
+ if audio_file:
141
+ play_and_download_audio(audio_file)
142
+ await broadcast_message(f"{username}|{message}", "snake_chat")
143
+ st.session_state.chat_history.append(entry)
144
+ st.session_state.last_chat_update = time.time()
145
+
146
+ async def load_chat():
147
+ with open(CHAT_FILE, 'r') as f:
148
+ content = f.read().strip()
149
+ return content.split('\n')
150
+
151
+ # Game HTML
152
+ html_code = f"""
153
+ <!DOCTYPE html>
154
+ <html lang="en">
155
+ <head>
156
+ <meta charset="UTF-8">
157
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
158
+ <title>Galaxian Snake 3D Multiplayer</title>
159
+ <style>
160
+ body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
161
+ #gameContainer {{ width: {container_width}px; height: {container_height}px; position: relative; }}
162
+ canvas {{ width: 100%; height: 100%; display: block; }}
163
+ .ui-container {{
164
+ position: absolute; top: 10px; left: 10px; color: white;
165
+ background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
166
+ user-select: none;
167
+ }}
168
+ .controls {{
169
+ position: absolute; bottom: 10px; left: 10px; color: white;
170
+ background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
171
+ }}
172
+ #chatBox {{
173
+ position: absolute; bottom: 60px; left: 10px; width: 300px; height: 200px;
174
+ background: rgba(0, 0, 0, 0.7); color: white; padding: 10px;
175
+ border-radius: 5px; overflow-y: auto;
176
+ }}
177
+ #debug {{ position: absolute; top: 10px; right: 10px; color: white; }}
178
+ </style>
179
+ </head>
180
+ <body>
181
+ <div id="gameContainer">
182
+ <div class="ui-container">
183
+ <h2>Galaxian Snake 3D</h2>
184
+ <div id="players">Players: 1</div>
185
+ <div id="score">Score: 0</div>
186
+ <div id="length">Length: 1</div>
187
+ <div id="survival">Survival: 0s</div>
188
+ <div id="shield">Shield: Off</div>
189
+ </div>
190
+ <div id="chatBox"></div>
191
+ <div class="controls">
192
+ <p>Controls: W/A/S/D or Arrows to steer, Space to shoot, Shift for shield</p>
193
+ <p>Eat food, shoot buildings, pass gates, dodge enemies!</p>
194
+ </div>
195
+ <div id="debug"></div>
196
+ </div>
197
+
198
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
199
+ <script>
200
+ try {{
201
+ console.log('Starting game initialization...');
202
+ let score = 0, players = {{}}, foodItems = [], buildings = [], bullets = [], debris = [], gates = [], enemies = [], enemyBullets = [];
203
+ let snakeSegments = [], moveLeft = false, moveRight = false, moveUp = false, moveDown = false, shoot = false, shield = false;
204
+ const playerName = "{st.session_state.username}";
205
+ let ws = new WebSocket('ws://localhost:8765');
206
+ const debug = document.getElementById('debug');
207
+ let survivalTime = 0, lastCollisionTime = performance.now(), shieldTimer = 0;
208
+
209
+ // Scene setup
210
+ const scene = new THREE.Scene();
211
+ const camera = new THREE.PerspectiveCamera(75, {container_width} / {container_height}, 0.1, 1000);
212
+ camera.position.set(0, 15, 20);
213
+ console.log('Camera initialized at:', camera.position);
214
+
215
+ const renderer = new THREE.WebGLRenderer({{ antialias: true }});
216
+ renderer.setSize({container_width}, {container_height});
217
+ renderer.shadowMap.enabled = true;
218
+ document.getElementById('gameContainer').appendChild(renderer.domElement);
219
+ console.log('Renderer initialized');
220
+
221
+ // Lighting
222
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
223
+ scene.add(ambientLight);
224
+ const sunLight = new THREE.DirectionalLight(0xffddaa, 1);
225
+ sunLight.position.set(50, 50, 50);
226
+ sunLight.castShadow = true;
227
+ scene.add(sunLight);
228
+ console.log('Lights added');
229
+
230
+ // Ground
231
+ const textureLoader = new THREE.TextureLoader();
232
+ const groundGeometry = new THREE.PlaneGeometry(200, 1000);
233
+ const groundMaterial = new THREE.MeshStandardMaterial({{
234
+ color: 0x1a5e1a,
235
+ bumpMap: textureLoader.load('https://threejs.org/examples/textures/terrain/grasslight-big-nm.jpg'),
236
+ bumpScale: 0.1
237
+ }});
238
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
239
+ ground.rotation.x = -Math.PI / 2;
240
+ ground.receiveShadow = true;
241
+ ground.position.z = -500;
242
+ scene.add(ground);
243
+ console.log('Ground added');
244
+
245
+ // Player (Snake Head and Segments)
246
+ const snakeGeometry = new THREE.BoxGeometry(1, 1, 1);
247
+ const snakeMaterial = new THREE.MeshPhongMaterial({{ color: 0x00ff00 }});
248
+ const head = new THREE.Mesh(snakeGeometry, snakeMaterial);
249
+ head.position.set(0, 0.5, 0);
250
+ head.castShadow = true;
251
+ snakeSegments.push({{ mesh: head, timer: -1, permanent: true }}); // Head is permanent
252
+ scene.add(head);
253
+ players[playerName] = {{ snake: snakeSegments, score: 0, length: 1 }};
254
+ console.log('Snake head initialized at:', head.position);
255
+
256
+ // Building rules
257
+ const buildingColors = [0x888888, 0x666666, 0x999999];
258
+ const buildingRules = [
259
+ {{name: "Simple", axiom: "F", rules: {{"F": "F[+F][-F]"}}, iterations: 1, baseHeight: 10, baseWidth: 5, baseDepth: 5, angle: Math.PI/4, probability: 1}}
260
+ ];
261
+
262
+ function interpretLSystem(rule, position, rotation) {{
263
+ let currentString = rule.axiom;
264
+ for (let i = 0; i < rule.iterations; i++) {{
265
+ let newString = "";
266
+ for (let j = 0; j < currentString.length; j++) {{
267
+ newString += rule.rules[currentString[j]] || currentString[j];
268
+ }}
269
+ currentString = newString;
270
+ }}
271
+
272
+ let buildingGroup = new THREE.Group();
273
+ buildingGroup.position.copy(position);
274
+ const stack = [];
275
+ let currentPosition = new THREE.Vector3(0, 0, 0);
276
+ let currentRotation = rotation || new THREE.Euler();
277
+ let scale = new THREE.Vector3(1, 1, 1);
278
+ const color = buildingColors[Math.floor(Math.random() * buildingColors.length)];
279
+ const material = new THREE.MeshStandardMaterial({{color: color}});
280
+
281
+ for (let i = 0; i < currentString.length; i++) {{
282
+ const char = currentString[i];
283
+ switch (char) {{
284
+ case 'F':
285
+ const width = rule.baseWidth * scale.x;
286
+ const height = rule.baseHeight * scale.y;
287
+ const depth = rule.baseDepth * scale.z;
288
+ const geometry = new THREE.BoxGeometry(width, height, depth);
289
+ const buildingPart = new THREE.Mesh(geometry, material);
290
+ buildingPart.position.copy(currentPosition);
291
+ buildingPart.position.y += height / 2;
292
+ buildingPart.rotation.copy(currentRotation);
293
+ buildingPart.castShadow = true;
294
+ buildingPart.receiveShadow = true;
295
+ buildingGroup.add(buildingPart);
296
+ const direction = new THREE.Vector3(0, height, 0);
297
+ direction.applyEuler(currentRotation);
298
+ currentPosition.add(direction);
299
+ break;
300
+ case '+': currentRotation.y += rule.angle; break;
301
+ case '-': currentRotation.y -= rule.angle; break;
302
+ case '[': stack.push({{position: currentPosition.clone(), rotation: currentRotation.clone(), scale: scale.clone()}}); break;
303
+ case ']': if (stack.length > 0) {{ const state = stack.pop(); currentPosition = state.position; currentRotation = state.rotation; scale = state.scale; }} break;
304
+ }}
305
+ }}
306
+ return buildingGroup;
307
+ }}
308
+
309
+ function spawnBuildings() {{
310
+ const cityWidth = 40, cityDepth = -1000;
311
+ for (let i = buildings.length; i < 20; i++) {{
312
+ const position = new THREE.Vector3(
313
+ Math.random() * cityWidth - cityWidth / 2,
314
+ 0,
315
+ cityDepth + Math.random() * 1000
316
+ );
317
+ const building = interpretLSystem(buildingRules[0], position, new THREE.Euler());
318
+ buildings.push(building);
319
+ scene.add(building);
320
+ console.log('Building at:', position);
321
+ }}
322
+ }}
323
+
324
+ function spawnFood() {{
325
+ if (foodItems.length < 10) {{
326
+ const food = new THREE.Mesh(
327
+ new THREE.BoxGeometry(1, 1, 1),
328
+ new THREE.MeshStandardMaterial({{color: 0xffff00}})
329
+ );
330
+ food.position.set(
331
+ Math.random() * 40 - 20,
332
+ Math.random() * 10,
333
+ -1000
334
+ );
335
+ foodItems.push(food);
336
+ scene.add(food);
337
+ console.log('Food at:', food.position);
338
+ }}
339
+ }}
340
+
341
+ function spawnGates() {{
342
+ if (gates.length < 5) {{
343
+ const gate = new THREE.Group();
344
+ const postGeometry = new THREE.CylinderGeometry(0.5, 0.5, 10, 8);
345
+ const postMaterial = new THREE.MeshPhongMaterial({{ color: 0xaaaaaa }});
346
+ const leftPost = new THREE.Mesh(postGeometry, postMaterial);
347
+ leftPost.position.set(-5, 5, 0);
348
+ const rightPost = new THREE.Mesh(postGeometry, postMaterial);
349
+ rightPost.position.set(5, 5, 0);
350
+ const fieldGeometry = new THREE.PlaneGeometry(10, 10);
351
+ const fieldMaterial = new THREE.MeshBasicMaterial({{ color: 0x00ffff, transparent: true, opacity: 0.5, side: THREE.DoubleSide }});
352
+ const field = new THREE.Mesh(fieldGeometry, fieldMaterial);
353
+ field.position.set(0, 5, 0);
354
+ gate.add(leftPost);
355
+ gate.add(rightPost);
356
+ gate.add(field);
357
+ gate.position.set(Math.random() * 40 - 20, 0, -1000 + Math.random() * 1000);
358
+ gates.push(gate);
359
+ scene.add(gate);
360
+ console.log('Gate at:', gate.position);
361
+ }}
362
+ }}
363
+
364
+ function spawnEnemyCluster() {{
365
+ const clusterCenter = new THREE.Vector3(
366
+ Math.random() * 40 - 20,
367
+ Math.random() * 10 + 5,
368
+ -1000
369
+ );
370
+ const enemyGeometry = new THREE.BoxGeometry(0.8, 0.8, 0.8);
371
+ const enemyMaterial = new THREE.MeshPhongMaterial({{ color: 0xff0000 }});
372
+ const pattern = [[-2, 1], [0, 1], [2, 1], [-1, 2], [1, 2]];
373
+ pattern.forEach(pos => {{
374
+ const enemy = new THREE.Mesh(enemyGeometry, enemyMaterial);
375
+ enemy.position.set(
376
+ clusterCenter.x + pos[0],
377
+ clusterCenter.y + pos[1],
378
+ clusterCenter.z
379
+ );
380
+ enemy.shootTimer = Math.random();
381
+ enemies.push(enemy);
382
+ scene.add(enemy);
383
+ }});
384
+ console.log('Enemy cluster at:', clusterCenter);
385
+ }}
386
+
387
+ function addTailSegment() {{
388
+ if (snakeSegments.length < 5) {{
389
+ const segment = new THREE.Mesh(snakeGeometry, snakeMaterial.clone());
390
+ const lastSegment = snakeSegments[snakeSegments.length - 1].mesh;
391
+ segment.position.copy(lastSegment.position);
392
+ segment.castShadow = true;
393
+ snakeSegments.push({{ mesh: segment, timer: 5, permanent: false }});
394
+ scene.add(segment);
395
+ players[playerName].length = snakeSegments.length;
396
+ console.log('Tail segment added, length:', snakeSegments.length);
397
+ }}
398
+ }}
399
+
400
+ function shootBullet(segment) {{
401
+ const bulletGeometry = new THREE.SphereGeometry(0.3, 8, 8);
402
+ const bulletMaterial = new THREE.MeshPhongMaterial({{ color: 0xff0000 }});
403
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
404
+ bullet.position.copy(segment.position);
405
+ bullet.velocity = new THREE.Vector3(0, 0, -20);
406
+ bullet.timer = 5;
407
+ bullets.push(bullet);
408
+ scene.add(bullet);
409
+ }}
410
+
411
+ function spawnExplosionParticles(position) {{
412
+ const particleGeometry = new THREE.SphereGeometry(0.2, 8, 8);
413
+ const particleMaterial = new THREE.MeshBasicMaterial({{ color: 0xffff00 }});
414
+ for (let i = 0; i < 20; i++) {{
415
+ const particle = new THREE.Mesh(particleGeometry, particleMaterial);
416
+ particle.position.copy(position);
417
+ particle.velocity = new THREE.Vector3(
418
+ Math.random() - 0.5,
419
+ Math.random() - 0.5,
420
+ Math.random() - 0.5
421
+ ).multiplyScalar(5);
422
+ particle.timer = 1;
423
+ debris.push(particle);
424
+ scene.add(particle);
425
+ }}
426
+ }}
427
+
428
+ function destroyBuilding(building, index) {{
429
+ building.children.forEach(child => {{
430
+ const debrisPiece = new THREE.Mesh(child.geometry, child.material);
431
+ debrisPiece.position.copy(child.getWorldPosition(new THREE.Vector3()));
432
+ debrisPiece.velocity = new THREE.Vector3(
433
+ Math.random() - 0.5,
434
+ Math.random() * 2,
435
+ Math.random() - 0.5
436
+ ).multiplyScalar(5);
437
+ debrisPiece.timer = 3;
438
+ debris.push(debrisPiece);
439
+ scene.add(debrisPiece);
440
+ }});
441
+ scene.remove(building);
442
+ buildings.splice(index, 1);
443
+ score += 100;
444
+ spawnBuildings();
445
+ }}
446
+
447
+ function shootEnemyBullet(enemy) {{
448
+ const bulletGeometry = new THREE.SphereGeometry(0.3, 8, 8);
449
+ const bulletMaterial = new THREE.MeshPhongMaterial({{ color: 0x00ff00 }});
450
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
451
+ bullet.position.copy(enemy.position);
452
+ const direction = snakeSegments[0].mesh.position.clone().sub(enemy.position).normalize();
453
+ bullet.velocity = direction.multiplyScalar(15);
454
+ bullet.timer = 5;
455
+ enemyBullets.push(bullet);
456
+ scene.add(bullet);
457
+ }}
458
+
459
+ // Controls
460
+ document.addEventListener('keydown', (event) => {{
461
+ switch (event.code) {{
462
+ case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
463
+ case 'ArrowRight': case 'KeyD': moveRight = true; break;
464
+ case 'ArrowUp': case 'KeyW': moveUp = true; break;
465
+ case 'ArrowDown': case 'KeyS': moveDown = true; break;
466
+ case 'Space': shoot = true; break;
467
+ case 'ShiftLeft': case 'ShiftRight': shield = true; shieldTimer = 4; break;
468
+ }}
469
+ }});
470
+ document.addEventListener('keyup', (event) => {{
471
+ switch (event.code) {{
472
+ case 'ArrowLeft': case 'KeyA': moveLeft = false; break;
473
+ case 'ArrowRight': case 'KeyD': moveRight = false; break;
474
+ case 'ArrowUp': case 'KeyW': moveUp = false; break;
475
+ case 'ArrowDown': case 'KeyS': moveDown = false; break;
476
+ case 'Space': shoot = false; break;
477
+ }}
478
+ }});
479
+
480
+ function updatePlayer(delta) {{
481
+ const speed = 10;
482
+ const head = snakeSegments[0].mesh;
483
+ if (moveLeft && head.position.x > -20) head.position.x -= speed * delta;
484
+ if (moveRight && head.position.x < 20) head.position.x += speed * delta;
485
+ if (moveUp && head.position.y < 15) head.position.y += speed * delta;
486
+ if (moveDown && head.position.y > 0.5) head.position.y -= speed * delta;
487
+
488
+ if (shoot && !bullets.some(b => b.timer > 4.8)) {{
489
+ snakeSegments.forEach(segment => shootBullet(segment.mesh));
490
+ }}
491
+ if (shieldTimer > 0) {{
492
+ shieldTimer -= delta;
493
+ snakeSegments.forEach(s => s.mesh.material.color.setHex(0x0000ff));
494
+ }} else {{
495
+ snakeSegments.forEach(s => s.mesh.material.color.setHex(0x00ff00));
496
+ }}
497
+
498
+ // Update tail positions
499
+ for (let i = snakeSegments.length - 1; i > 0; i--) {{
500
+ const current = snakeSegments[i].mesh;
501
+ const previous = snakeSegments[i - 1].mesh;
502
+ current.position.lerp(previous.position, 0.5);
503
+ if (!snakeSegments[i].permanent) {{
504
+ snakeSegments[i].timer -= delta;
505
+ if (snakeSegments[i].timer <= 0) {{
506
+ scene.remove(current);
507
+ snakeSegments.splice(i, 1);
508
+ players[playerName].length = snakeSegments.length;
509
+ }}
510
+ }}
511
+ }}
512
+
513
+ const forwardSpeed = 20;
514
+ buildings.forEach((building, i) => {{
515
+ building.position.z += forwardSpeed * delta;
516
+ if (building.position.z > 20) {{
517
+ building.position.z = -1000;
518
+ building.position.x = Math.random() * 40 - 20;
519
+ }}
520
+ if (head.position.distanceTo(building.position) < 5) {{
521
+ head.position.set(0, 0.5, 0);
522
+ lastCollisionTime = performance.now();
523
+ }}
524
+ }});
525
+ foodItems.forEach((food, i) => {{
526
+ food.position.z += forwardSpeed * delta;
527
+ if (food.position.z > 20) {{
528
+ scene.remove(food);
529
+ foodItems.splice(i, 1);
530
+ spawnFood();
531
+ }}
532
+ if (head.position.distanceTo(food.position) < 1) {{
533
+ scene.remove(food);
534
+ foodItems.splice(i, 1);
535
+ score += 10;
536
+ players[playerName].length = snakeSegments.length;
537
+ spawnFood();
538
+ }}
539
+ }});
540
+ gates.forEach((gate, i) => {{
541
+ gate.position.z += forwardSpeed * delta;
542
+ if (gate.position.z > 20) {{
543
+ scene.remove(gate);
544
+ gates.splice(i, 1);
545
+ spawnGates();
546
+ }}
547
+ const fieldPos = gate.children[2].getWorldPosition(new THREE.Vector3());
548
+ if (head.position.distanceTo(fieldPos) < 5) {{
549
+ score += 100;
550
+ addTailSegment();
551
+ for (let j = 0; j < snakeSegments.length; j++) {{
552
+ if (!snakeSegments[j].permanent && snakeSegments[j].timer > 0) {{
553
+ snakeSegments[j].permanent = true;
554
+ snakeSegments[j].timer = -1;
555
+ score += 20;
556
+ spawnExplosionParticles(fieldPos);
557
+ break;
558
+ }}
559
+ }}
560
+ scene.remove(gate);
561
+ gates.splice(i, 1);
562
+ spawnGates();
563
+ }}
564
+ }});
565
+
566
+ camera.position.set(head.position.x, head.position.y + 15, head.position.z + 20);
567
+ camera.lookAt(head.position);
568
+ }}
569
+
570
+ function updateBullets(delta) {{
571
+ for (let i = bullets.length - 1; i >= 0; i--) {{
572
+ const bullet = bullets[i];
573
+ bullet.position.add(bullet.velocity.clone().multiplyScalar(delta));
574
+ bullet.timer -= delta;
575
+
576
+ if (bullet.timer <= 0) {{
577
+ scene.remove(bullet);
578
+ bullets.splice(i, 1);
579
+ continue;
580
+ }}
581
+
582
+ for (let j = buildings.length - 1; j >= 0; j--) {{
583
+ if (bullet.position.distanceTo(buildings[j].position) < 5) {{
584
+ destroyBuilding(buildings[j], j);
585
+ scene.remove(bullet);
586
+ bullets.splice(i, 1);
587
+ break;
588
+ }}
589
+ }}
590
+ }}
591
+ }}
592
+
593
+ function updateDebris(delta) {{
594
+ for (let i = debris.length - 1; i >= 0; i--) {{
595
+ const piece = debris[i];
596
+ piece.position.add(piece.velocity.clone().multiplyScalar(delta));
597
+ piece.velocity.y -= 9.8 * delta;
598
+ piece.timer -= delta;
599
+
600
+ if (piece.position.y <= 0) {{
601
+ piece.velocity.y *= -0.7;
602
+ piece.position.y = 0;
603
+ }}
604
+ if (piece.timer <= 0) {{
605
+ scene.remove(piece);
606
+ debris.splice(i, 1);
607
+ }}
608
+ }}
609
+ }}
610
+
611
+ function updateEnemies(delta) {{
612
+ enemies.forEach((enemy, i) => {{
613
+ enemy.position.z += 10 * delta;
614
+ enemy.shootTimer -= delta;
615
+ if (enemy.shootTimer <= 0) {{
616
+ shootEnemyBullet(enemy);
617
+ enemy.shootTimer = 1;
618
+ }}
619
+ if (enemy.position.z > 20) {{
620
+ scene.remove(enemy);
621
+ enemies.splice(i, 1);
622
+ }}
623
+ }});
624
+ if (enemies.length < 10) spawnEnemyCluster();
625
+ }}
626
+
627
+ function updateEnemyBullets(delta) {{
628
+ for (let i = enemyBullets.length - 1; i >= 0; i--) {{
629
+ const bullet = enemyBullets[i];
630
+ const direction = snakeSegments[0].mesh.position.clone().sub(bullet.position).normalize();
631
+ bullet.velocity.lerp(direction.multiplyScalar(15), delta * 2);
632
+ bullet.position.add(bullet.velocity.clone().multiplyScalar(delta));
633
+ bullet.timer -= delta;
634
+
635
+ if (bullet.timer <= 0 || bullet.position.z > 20) {{
636
+ scene.remove(bullet);
637
+ enemyBullets.splice(i, 1);
638
+ continue;
639
+ }}
640
+
641
+ if (bullet.position.distanceTo(snakeSegments[0].mesh.position) < 1 && shieldTimer <= 0) {{
642
+ snakeSegments[0].mesh.position.set(0, 0.5, 0);
643
+ lastCollisionTime = performance.now();
644
+ scene.remove(bullet);
645
+ enemyBullets.splice(i, 1);
646
+ }} else if (bullet.position.distanceTo(snakeSegments[0].mesh.position) < 1 && shieldTimer > 0) {{
647
+ scene.remove(bullet);
648
+ enemyBullets.splice(i, 1);
649
+ }}
650
+ }}
651
+ }}
652
+
653
+ function updateUI() {{
654
+ survivalTime = (performance.now() - lastCollisionTime) / 1000;
655
+ score += Math.floor(survivalTime / 10);
656
+ document.getElementById('players').textContent = `Players: ${{Object.keys(players).length}}`;
657
+ document.getElementById('score').textContent = `Score: ${{score}}`;
658
+ document.getElementById('length').textContent = `Length: ${{snakeSegments.length}}`;
659
+ document.getElementById('survival').textContent = `Survival: ${{survivalTime.toFixed(1)}}s`;
660
+ document.getElementById('shield').textContent = `Shield: ${{shieldTimer > 0 ? 'On' : 'Off'}}`;
661
+ debug.innerHTML = `Scene: ${{scene.children.length}}<br>Buildings: ${{buildings.length}}<br>Food: ${{foodItems.length}}<br>Bullets: ${{bullets.length}}<br>Gates: ${{gates.length}}<br>Enemies: ${{enemies.length}}`;
662
+ }}
663
+
664
+ // WebSocket chat
665
+ ws.onmessage = function(event) {{
666
+ const [sender, message] = event.data.split('|');
667
+ const chatBox = document.getElementById('chatBox');
668
+ chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
669
+ chatBox.scrollTop = chatBox.scrollHeight;
670
+ }};
671
+
672
+ ws.onopen = function() {{ console.log('Connected to WebSocket server'); }};
673
+ ws.onerror = function(error) {{ console.error('WebSocket error:', error); }};
674
+ ws.onclose = function() {{ console.log('Disconnected from WebSocket server'); }};
675
+
676
+ // Game loop
677
+ let lastTime = performance.now();
678
+ function animate() {{
679
+ requestAnimationFrame(animate);
680
+ const currentTime = performance.now();
681
+ const delta = (currentTime - lastTime) / 1000;
682
+ lastTime = currentTime;
683
+
684
+ updatePlayer(delta);
685
+ updateBullets(delta);
686
+ updateDebris(delta);
687
+ updateEnemies(delta);
688
+ updateEnemyBullets(delta);
689
+ spawnFood();
690
+ spawnGates();
691
+ updateUI();
692
+
693
+ renderer.render(scene, camera);
694
+ }}
695
+
696
+ // Initialize
697
+ spawnBuildings();
698
+ spawnFood();
699
+ spawnGates();
700
+ spawnEnemyCluster();
701
+ animate();
702
+ console.log('Game initialized');
703
+ }} catch (e) {{
704
+ console.error('Error in game initialization:', e);
705
+ document.getElementById('debug').innerHTML = 'Error: ' + e.message;
706
+ }}
707
+ </script>
708
+ </body>
709
+ </html>
710
+ """
711
+
712
+ # Render the HTML component
713
+ components.html(html_code, width=container_width, height=container_height)
714
+
715
+ # Chat Interface
716
+ st.sidebar.title(f"Chat as {st.session_state.username}")
717
+ chat_content = asyncio.run(load_chat())
718
+ chat_container = st.sidebar.container()
719
+ with chat_container:
720
+ st.code("\n".join(chat_content), language="python")
721
+
722
+ message = st.sidebar.text_input("Message", key="chat_input")
723
+ if message and st.sidebar.button("Send 🚀"):
724
+ asyncio.run(save_chat_entry(st.session_state.username, message))
725
+
726
+ audio_bytes = audio_recorder()
727
+ if audio_bytes:
728
+ with open("temp_audio.wav", "wb") as f:
729
+ f.write(audio_bytes)
730
+ message = "Voice message received"
731
+ asyncio.run(save_chat_entry(st.session_state.username, message))
732
+
733
+ # Start WebSocket Server in Main Event Loop
734
+ async def main_async():
735
+ if not st.session_state.get('server_running', False):
736
+ await start_websocket_server()
737
+
738
+ loop = asyncio.get_event_loop()
739
+ if not st.session_state.get('server_running', False):
740
+ loop.run_until_complete(main_async())
741
+
742
+ st.sidebar.write("""
743
+ ### How to Play
744
+ - **W/A/S/D or Arrow Keys**: Steer the snake (up/down/left/right)
745
+ - **Space**: Shoot bullets from all segments to destroy buildings
746
+ - **Shift**: Activate 4s shield to block enemy bullets
747
+ - Eat yellow cubes for 10 points
748
+ - Destroy buildings for 100 points each
749
+ - Pass through gates for 100 points + tail segment (up to 5)
750
+ - Cross gate again within 5s for permanent segment + 20 points + explosion
751
+ - Survive longer for bonus points (1 per 10s)
752
+ - Dodge heat-seeking enemy bullets
753
+ - Chat with other players in real-time
754
+ """)