Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -20,7 +20,7 @@ nest_asyncio.apply()
|
|
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 with
|
24 |
|
25 |
# Sliders for container size
|
26 |
max_width = min(1200, st.session_state.get('window_width', 1200))
|
@@ -127,7 +127,7 @@ async def start_websocket_server():
|
|
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):
|
@@ -157,8 +157,8 @@ html_code = f"""
|
|
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; }}
|
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;
|
@@ -184,11 +184,10 @@ html_code = f"""
|
|
184 |
<div id="players">Players: 1</div>
|
185 |
<div id="score">Score: 0</div>
|
186 |
<div id="length">Length: 3</div>
|
187 |
-
<div id="leaderboard"></div>
|
188 |
</div>
|
189 |
<div id="chatBox"></div>
|
190 |
<div class="controls">
|
191 |
-
<p>Controls: W/A/S/D or Arrow Keys to
|
192 |
<p>Eat yellow cubes to grow and score!</p>
|
193 |
</div>
|
194 |
<div id="debug"></div>
|
@@ -198,18 +197,16 @@ html_code = f"""
|
|
198 |
<script>
|
199 |
try {{
|
200 |
console.log('Starting game initialization...');
|
201 |
-
|
202 |
-
let
|
203 |
-
|
204 |
-
const initialLength = 3, playerName = "{st.session_state.username}";
|
205 |
-
const highScores = JSON.parse(localStorage.getItem('highScores')) || [];
|
206 |
let ws = new WebSocket('ws://localhost:8765');
|
207 |
const debug = document.getElementById('debug');
|
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 }});
|
@@ -229,7 +226,7 @@ html_code = f"""
|
|
229 |
|
230 |
// Ground
|
231 |
const textureLoader = new THREE.TextureLoader();
|
232 |
-
const groundGeometry = new THREE.PlaneGeometry(200,
|
233 |
const groundMaterial = new THREE.MeshStandardMaterial({{
|
234 |
color: 0x1a5e1a,
|
235 |
bumpMap: textureLoader.load('https://threejs.org/examples/textures/terrain/grasslight-big-nm.jpg'),
|
@@ -238,9 +235,20 @@ html_code = f"""
|
|
238 |
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
239 |
ground.rotation.x = -Math.PI / 2;
|
240 |
ground.receiveShadow = true;
|
|
|
241 |
scene.add(ground);
|
242 |
console.log('Ground added');
|
243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
244 |
// Building rules
|
245 |
const buildingColors = [0x888888, 0x666666, 0x999999];
|
246 |
const buildingRules = [
|
@@ -294,36 +302,18 @@ html_code = f"""
|
|
294 |
return buildingGroup;
|
295 |
}}
|
296 |
|
297 |
-
function
|
298 |
-
const
|
299 |
-
for (let
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
scene.add(building);
|
305 |
-
buildings.push(building);
|
306 |
-
console.log('Building at:', position);
|
307 |
-
}}
|
308 |
-
}}
|
309 |
-
const roadWidth = 12; // Wider roads
|
310 |
-
for (let x = -citySize; x <= citySize; x++) {{
|
311 |
-
const road = new THREE.Mesh(
|
312 |
-
new THREE.PlaneGeometry(roadWidth, citySize * 2 * spacing + roadWidth),
|
313 |
-
new THREE.MeshStandardMaterial({{color: 0x333333}})
|
314 |
);
|
315 |
-
|
316 |
-
|
317 |
-
scene.add(
|
318 |
-
|
319 |
-
for (let z = -citySize; z <= citySize; z++) {{
|
320 |
-
const road = new THREE.Mesh(
|
321 |
-
new THREE.PlaneGeometry(citySize * 2 * spacing + roadWidth, roadWidth),
|
322 |
-
new THREE.MeshStandardMaterial({{color: 0x333333}})
|
323 |
-
);
|
324 |
-
road.rotation.x = -Math.PI / 2;
|
325 |
-
road.position.set(0, 0.01, z * spacing);
|
326 |
-
scene.add(road);
|
327 |
}}
|
328 |
}}
|
329 |
|
@@ -333,120 +323,79 @@ html_code = f"""
|
|
333 |
new THREE.BoxGeometry(1, 1, 1),
|
334 |
new THREE.MeshStandardMaterial({{color: 0xffff00}})
|
335 |
);
|
336 |
-
|
337 |
-
|
338 |
-
|
|
|
|
|
339 |
foodItems.push(food);
|
340 |
scene.add(food);
|
341 |
console.log('Food at:', food.position);
|
342 |
}}
|
343 |
}}
|
344 |
|
345 |
-
function resetSnake() {{
|
346 |
-
snake.forEach(seg => scene.remove(seg));
|
347 |
-
snake = [];
|
348 |
-
const snakeMaterial = new THREE.MeshStandardMaterial({{color: 0x00ff00}});
|
349 |
-
for (let i = 0; i < initialLength; i++) {{
|
350 |
-
const segment = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 16), snakeMaterial);
|
351 |
-
segment.position.set(-i * 1.2, 0.5, 0);
|
352 |
-
segment.castShadow = true;
|
353 |
-
snake.push(segment);
|
354 |
-
scene.add(segment);
|
355 |
-
}}
|
356 |
-
moveDir.set(1, 0, 0);
|
357 |
-
players[playerName] = {{ snake: snake, score: 0, length: initialLength }};
|
358 |
-
console.log('Snake reset at:', snake[0].position);
|
359 |
-
}}
|
360 |
-
|
361 |
// Controls
|
362 |
-
const keys = {{w: false, a: false, s: false, d: false}};
|
363 |
document.addEventListener('keydown', (event) => {{
|
364 |
-
switch (event.
|
365 |
-
case '
|
366 |
-
case '
|
367 |
-
case '
|
368 |
-
case '
|
369 |
}}
|
370 |
}});
|
371 |
document.addEventListener('keyup', (event) => {{
|
372 |
-
switch (event.
|
373 |
-
case '
|
374 |
-
case '
|
375 |
-
case '
|
376 |
-
case '
|
377 |
}}
|
378 |
}});
|
379 |
|
380 |
-
function
|
381 |
-
|
382 |
-
if (
|
383 |
-
|
384 |
-
|
385 |
-
if (
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
if (Math.abs(newHead.position.x) > 100 || Math.abs(newHead.position.z) > 100) {{
|
396 |
-
resetSnake();
|
397 |
-
return;
|
398 |
-
}}
|
399 |
-
|
400 |
-
for (let i = 1; i < snake.length; i++) {{
|
401 |
-
if (newHead.position.distanceTo(snake[i].position) < 0.9) {{
|
402 |
-
resetSnake();
|
403 |
-
return;
|
404 |
}}
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
headPos.z + 0.5 > buildingBox.min.z && headPos.z - 0.5 < buildingBox.max.z) {{
|
414 |
-
resetSnake();
|
415 |
-
return;
|
416 |
-
}}
|
417 |
-
}}
|
418 |
-
}});
|
419 |
-
}}
|
420 |
-
|
421 |
-
snake.unshift(newHead);
|
422 |
-
scene.add(newHead);
|
423 |
-
|
424 |
-
for (let i = foodItems.length - 1; i >= 0; i--) {{
|
425 |
-
if (newHead.position.distanceTo(foodItems[i].position) < 1) {{
|
426 |
-
scene.remove(foodItems[i]);
|
427 |
foodItems.splice(i, 1);
|
428 |
spawnFood();
|
429 |
-
score += 2;
|
430 |
-
players[playerName].score = score;
|
431 |
-
players[playerName].length = snake.length;
|
432 |
-
updateUI();
|
433 |
-
break;
|
434 |
-
}} else {{
|
435 |
-
const tail = snake.pop();
|
436 |
-
scene.remove(tail);
|
437 |
}}
|
438 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
439 |
|
440 |
-
|
441 |
-
camera.
|
442 |
-
camera.lookAt(headPos);
|
443 |
}}
|
444 |
|
445 |
function updateUI() {{
|
446 |
document.getElementById('players').textContent = `Players: ${{Object.keys(players).length}}`;
|
447 |
document.getElementById('score').textContent = `Score: ${{score}}`;
|
448 |
-
document.getElementById('length').textContent = `Length: ${{
|
449 |
-
debug.innerHTML = `Scene: ${{scene.children.length}}<br>Buildings: ${{buildings.length}}<br>
|
450 |
}}
|
451 |
|
452 |
// WebSocket chat
|
@@ -469,7 +418,7 @@ html_code = f"""
|
|
469 |
const delta = (currentTime - lastTime) / 1000;
|
470 |
lastTime = currentTime;
|
471 |
|
472 |
-
|
473 |
spawnFood();
|
474 |
updateUI();
|
475 |
|
@@ -477,8 +426,7 @@ html_code = f"""
|
|
477 |
}}
|
478 |
|
479 |
// Initialize
|
480 |
-
|
481 |
-
createCity();
|
482 |
spawnFood();
|
483 |
animate();
|
484 |
console.log('Game initialized');
|
@@ -523,8 +471,8 @@ if not st.session_state.get('server_running', False):
|
|
523 |
|
524 |
st.sidebar.write("""
|
525 |
### How to Play
|
526 |
-
- **W/A/S/D or Arrow Keys**:
|
527 |
- Eat yellow cubes to grow and score
|
528 |
-
-
|
529 |
- Chat with other players in real-time
|
530 |
""")
|
|
|
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 with continuous motion, eat food, and chat in real-time!")
|
24 |
|
25 |
# Sliders for container size
|
26 |
max_width = min(1200, st.session_state.get('window_width', 1200))
|
|
|
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):
|
|
|
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;
|
|
|
184 |
<div id="players">Players: 1</div>
|
185 |
<div id="score">Score: 0</div>
|
186 |
<div id="length">Length: 3</div>
|
|
|
187 |
</div>
|
188 |
<div id="chatBox"></div>
|
189 |
<div class="controls">
|
190 |
+
<p>Controls: W/A/S/D or Arrow Keys to steer</p>
|
191 |
<p>Eat yellow cubes to grow and score!</p>
|
192 |
</div>
|
193 |
<div id="debug"></div>
|
|
|
197 |
<script>
|
198 |
try {{
|
199 |
console.log('Starting game initialization...');
|
200 |
+
let score = 0, players = {{}}, foodItems = [], buildings = [];
|
201 |
+
let snake, moveLeft = false, moveRight = false, moveUp = false, moveDown = false;
|
202 |
+
const playerName = "{st.session_state.username}";
|
|
|
|
|
203 |
let ws = new WebSocket('ws://localhost:8765');
|
204 |
const debug = document.getElementById('debug');
|
205 |
|
206 |
// Scene setup
|
207 |
const scene = new THREE.Scene();
|
208 |
const camera = new THREE.PerspectiveCamera(75, {container_width} / {container_height}, 0.1, 1000);
|
209 |
+
camera.position.set(0, 15, 20);
|
210 |
console.log('Camera initialized at:', camera.position);
|
211 |
|
212 |
const renderer = new THREE.WebGLRenderer({{ antialias: true }});
|
|
|
226 |
|
227 |
// Ground
|
228 |
const textureLoader = new THREE.TextureLoader();
|
229 |
+
const groundGeometry = new THREE.PlaneGeometry(200, 1000); // Long scrolling ground
|
230 |
const groundMaterial = new THREE.MeshStandardMaterial({{
|
231 |
color: 0x1a5e1a,
|
232 |
bumpMap: textureLoader.load('https://threejs.org/examples/textures/terrain/grasslight-big-nm.jpg'),
|
|
|
235 |
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
236 |
ground.rotation.x = -Math.PI / 2;
|
237 |
ground.receiveShadow = true;
|
238 |
+
ground.position.z = -500; // Start far back
|
239 |
scene.add(ground);
|
240 |
console.log('Ground added');
|
241 |
|
242 |
+
// Player (Snake Head)
|
243 |
+
const snakeGeometry = new THREE.BoxGeometry(1, 1, 1);
|
244 |
+
const snakeMaterial = new THREE.MeshPhongMaterial({{ color: 0x00ff00 }});
|
245 |
+
snake = new THREE.Mesh(snakeGeometry, snakeMaterial);
|
246 |
+
snake.position.set(0, 0.5, 0);
|
247 |
+
snake.castShadow = true;
|
248 |
+
scene.add(snake);
|
249 |
+
players[playerName] = {{ snake: snake, score: 0, length: 1 }};
|
250 |
+
console.log('Snake initialized at:', snake.position);
|
251 |
+
|
252 |
// Building rules
|
253 |
const buildingColors = [0x888888, 0x666666, 0x999999];
|
254 |
const buildingRules = [
|
|
|
302 |
return buildingGroup;
|
303 |
}}
|
304 |
|
305 |
+
function spawnBuildings() {{
|
306 |
+
const cityWidth = 40, cityDepth = -1000;
|
307 |
+
for (let i = buildings.length; i < 20; i++) {{
|
308 |
+
const position = new THREE.Vector3(
|
309 |
+
Math.random() * cityWidth - cityWidth / 2,
|
310 |
+
0,
|
311 |
+
cityDepth + Math.random() * 1000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
312 |
);
|
313 |
+
const building = interpretLSystem(buildingRules[0], position, new THREE.Euler());
|
314 |
+
buildings.push(building);
|
315 |
+
scene.add(building);
|
316 |
+
console.log('Building at:', position);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
317 |
}}
|
318 |
}}
|
319 |
|
|
|
323 |
new THREE.BoxGeometry(1, 1, 1),
|
324 |
new THREE.MeshStandardMaterial({{color: 0xffff00}})
|
325 |
);
|
326 |
+
food.position.set(
|
327 |
+
Math.random() * 40 - 20,
|
328 |
+
Math.random() * 10,
|
329 |
+
-1000
|
330 |
+
);
|
331 |
foodItems.push(food);
|
332 |
scene.add(food);
|
333 |
console.log('Food at:', food.position);
|
334 |
}}
|
335 |
}}
|
336 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
337 |
// Controls
|
|
|
338 |
document.addEventListener('keydown', (event) => {{
|
339 |
+
switch (event.code) {{
|
340 |
+
case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
|
341 |
+
case 'ArrowRight': case 'KeyD': moveRight = true; break;
|
342 |
+
case 'ArrowUp': case 'KeyW': moveUp = true; break;
|
343 |
+
case 'ArrowDown': case 'KeyS': moveDown = true; break;
|
344 |
}}
|
345 |
}});
|
346 |
document.addEventListener('keyup', (event) => {{
|
347 |
+
switch (event.code) {{
|
348 |
+
case 'ArrowLeft': case 'KeyA': moveLeft = false; break;
|
349 |
+
case 'ArrowRight': case 'KeyD': moveRight = false; break;
|
350 |
+
case 'ArrowUp': case 'KeyW': moveUp = false; break;
|
351 |
+
case 'ArrowDown': case 'KeyS': moveDown = false; break;
|
352 |
}}
|
353 |
}});
|
354 |
|
355 |
+
function updatePlayer(delta) {{
|
356 |
+
const speed = 10;
|
357 |
+
if (moveLeft && snake.position.x > -20) snake.position.x -= speed * delta;
|
358 |
+
if (moveRight && snake.position.x < 20) snake.position.x += speed * delta;
|
359 |
+
if (moveUp && snake.position.y < 15) snake.position.y += speed * delta;
|
360 |
+
if (moveDown && snake.position.y > 0.5) snake.position.y -= speed * delta;
|
361 |
+
|
362 |
+
// Continuous forward motion
|
363 |
+
const forwardSpeed = 20;
|
364 |
+
buildings.forEach(building => {{
|
365 |
+
building.position.z += forwardSpeed * delta;
|
366 |
+
if (building.position.z > 20) {{
|
367 |
+
building.position.z = -1000;
|
368 |
+
building.position.x = Math.random() * 40 - 20;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
369 |
}}
|
370 |
+
if (snake.position.distanceTo(building.position) < 5) {{
|
371 |
+
snake.position.set(0, 0.5, 0); // Reset on collision
|
372 |
+
}}
|
373 |
+
}});
|
374 |
+
foodItems.forEach((food, i) => {{
|
375 |
+
food.position.z += forwardSpeed * delta;
|
376 |
+
if (food.position.z > 20) {{
|
377 |
+
scene.remove(food);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
378 |
foodItems.splice(i, 1);
|
379 |
spawnFood();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
380 |
}}
|
381 |
+
if (snake.position.distanceTo(food.position) < 1) {{
|
382 |
+
scene.remove(food);
|
383 |
+
foodItems.splice(i, 1);
|
384 |
+
score += 10;
|
385 |
+
players[playerName].length += 1;
|
386 |
+
spawnFood();
|
387 |
+
}}
|
388 |
+
}});
|
389 |
|
390 |
+
camera.position.set(snake.position.x, snake.position.y + 15, snake.position.z + 20);
|
391 |
+
camera.lookAt(snake.position);
|
|
|
392 |
}}
|
393 |
|
394 |
function updateUI() {{
|
395 |
document.getElementById('players').textContent = `Players: ${{Object.keys(players).length}}`;
|
396 |
document.getElementById('score').textContent = `Score: ${{score}}`;
|
397 |
+
document.getElementById('length').textContent = `Length: ${{players[playerName].length}}`;
|
398 |
+
debug.innerHTML = `Scene: ${{scene.children.length}}<br>Buildings: ${{buildings.length}}<br>Food: ${{foodItems.length}}`;
|
399 |
}}
|
400 |
|
401 |
// WebSocket chat
|
|
|
418 |
const delta = (currentTime - lastTime) / 1000;
|
419 |
lastTime = currentTime;
|
420 |
|
421 |
+
updatePlayer(delta);
|
422 |
spawnFood();
|
423 |
updateUI();
|
424 |
|
|
|
426 |
}}
|
427 |
|
428 |
// Initialize
|
429 |
+
spawnBuildings();
|
|
|
430 |
spawnFood();
|
431 |
animate();
|
432 |
console.log('Game initialized');
|
|
|
471 |
|
472 |
st.sidebar.write("""
|
473 |
### How to Play
|
474 |
+
- **W/A/S/D or Arrow Keys**: Steer the snake (up/down/left/right)
|
475 |
- Eat yellow cubes to grow and score
|
476 |
+
- World moves continuously toward you
|
477 |
- Chat with other players in real-time
|
478 |
""")
|