Update app.py
Browse files
app.py
CHANGED
@@ -1,113 +1,73 @@
|
|
1 |
import streamlit as st
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
st.
|
16 |
-
|
17 |
-
st.
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
st.
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
# Define the HTML content with Three.js, injecting player_name
|
27 |
-
game_html = f"""
|
28 |
<!DOCTYPE html>
|
29 |
-
<html
|
30 |
<head>
|
31 |
-
<meta charset="
|
32 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
33 |
<title>Galaxian Snake 3D</title>
|
34 |
<style>
|
35 |
-
body {{ margin: 0; overflow: hidden;
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
|
|
|
|
|
|
45 |
}}
|
|
|
46 |
#gameOver {{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: red; font-size: 48px; z-index: 1; display: none; }}
|
47 |
</style>
|
48 |
</head>
|
49 |
<body>
|
50 |
-
<div
|
51 |
-
|
52 |
-
<
|
53 |
-
<div id="
|
54 |
-
<
|
55 |
-
<
|
56 |
-
|
57 |
-
|
58 |
-
<p>Controls: W/A/S/D or Arrow Keys to move, R to reset</p>
|
59 |
-
<p>Eat yellow cubes to grow and score points!</p>
|
60 |
</div>
|
61 |
<div id="gameOver">Game Over</div>
|
62 |
-
|
63 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
64 |
<script>
|
65 |
-
|
66 |
let score = 0, gameTime = 0, lives = 3, gameActive = true;
|
67 |
-
let snake, foodItems = [], lSysCreatures = [], quineAgents = [], buildings = [];
|
68 |
const playerName = "{player_name}";
|
69 |
const maxGameTime = 300; // 5 minutes in seconds
|
70 |
let initialLength = 3, lastLength = 3, moveCounter = 0, moveInterval = 0.1;
|
71 |
let moveDir = new THREE.Vector3(1, 0, 0);
|
72 |
-
|
73 |
-
// Scene setup
|
74 |
-
const scene = new THREE.Scene();
|
75 |
-
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
76 |
-
camera.position.set(0, 20, 30);
|
77 |
-
const renderer = new THREE.WebGLRenderer({{ antialias: true }});
|
78 |
-
renderer.setSize(window.innerWidth, window.innerHeight);
|
79 |
-
renderer.shadowMap.enabled = true;
|
80 |
-
document.body.appendChild(renderer.domElement);
|
81 |
-
|
82 |
-
// Lighting
|
83 |
-
const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
|
84 |
-
scene.add(ambientLight);
|
85 |
-
const sunLight = new THREE.DirectionalLight(0xffddaa, 0.8);
|
86 |
-
sunLight.castShadow = true;
|
87 |
-
sunLight.shadow.mapSize.width = 2048;
|
88 |
-
sunLight.shadow.mapSize.height = 2048;
|
89 |
-
sunLight.shadow.camera.near = 1;
|
90 |
-
sunLight.shadow.camera.far = 500;
|
91 |
-
sunLight.shadow.camera.left = -100;
|
92 |
-
sunLight.shadow.camera.right = 100;
|
93 |
-
sunLight.shadow.camera.top = 100;
|
94 |
-
sunLight.shadow.camera.bottom = -100;
|
95 |
-
scene.add(sunLight);
|
96 |
-
|
97 |
-
// Ground
|
98 |
-
const textureLoader = new THREE.TextureLoader();
|
99 |
-
const groundGeometry = new THREE.PlaneGeometry(200, 200);
|
100 |
-
const groundMaterial = new THREE.MeshStandardMaterial({{
|
101 |
-
color: 0x1a5e1a,
|
102 |
-
roughness: 0.8,
|
103 |
-
metalness: 0.2,
|
104 |
-
bumpMap: textureLoader.load('https://threejs.org/examples/textures/terrain/grasslight-big-nm.jpg'),
|
105 |
-
bumpScale: 0.1
|
106 |
-
}});
|
107 |
-
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
108 |
-
ground.rotation.x = -Math.PI / 2;
|
109 |
-
ground.receiveShadow = true;
|
110 |
-
scene.add(ground);
|
111 |
|
112 |
// Building rules
|
113 |
const buildingRules = [
|
@@ -119,6 +79,71 @@ game_html = f"""
|
|
119 |
];
|
120 |
const buildingColors = [0x888888, 0x666666, 0x999999, 0xaaaaaa, 0x555555, 0x334455, 0x445566, 0x223344, 0x556677, 0x667788, 0x993333, 0x884422, 0x553333, 0x772222, 0x664433];
|
121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
function interpretLSystem(rule, position, rotation) {{
|
123 |
let currentString = rule.axiom;
|
124 |
for (let i = 0; i < rule.iterations; i++) {{
|
@@ -138,8 +163,7 @@ game_html = f"""
|
|
138 |
const color = buildingColors[Math.floor(Math.random() * buildingColors.length)];
|
139 |
const material = new THREE.MeshStandardMaterial({{color: color, roughness: 0.7, metalness: 0.2}});
|
140 |
|
141 |
-
for (let
|
142 |
-
const char = currentString[i];
|
143 |
switch (char) {{
|
144 |
case 'F':
|
145 |
const width = rule.baseWidth * (0.5 + Math.random() * 0.5) * scale.x;
|
@@ -278,11 +302,11 @@ game_html = f"""
|
|
278 |
angle: Math.random() * Math.PI * 2,
|
279 |
speed: 0.5 + Math.random() * 0.5,
|
280 |
radius: 3 + Math.random() * 2,
|
281 |
-
ttl: 5
|
282 |
}};
|
283 |
quineAgents.push(agent);
|
284 |
scene.add(agent);
|
285 |
-
setTimeout(() => lSysCreatures.forEach(c => c !== sender && listenToAgent(c
|
286 |
}}
|
287 |
|
288 |
function listenToAgent(creature) {{
|
@@ -295,32 +319,11 @@ game_html = f"""
|
|
295 |
setTimeout(() => scene.remove(response), 2000);
|
296 |
}}
|
297 |
|
298 |
-
function updateQuineAgents(delta) {{
|
299 |
-
for (let i = quineAgents.length - 1; i >= 0; i--) {{
|
300 |
-
const agent = quineAgents[i];
|
301 |
-
agent.userData.ttl -= delta;
|
302 |
-
if (agent.userData.ttl <= 0) {{
|
303 |
-
scene.remove(agent);
|
304 |
-
quineAgents.splice(i, 1);
|
305 |
-
continue;
|
306 |
-
}}
|
307 |
-
// Orbit around origin
|
308 |
-
agent.userData.angle += agent.userData.speed * delta;
|
309 |
-
const offsetX = Math.cos(agent.userData.angle) * agent.userData.radius;
|
310 |
-
const offsetZ = Math.sin(agent.userData.angle) * agent.userData.radius;
|
311 |
-
agent.position.set(
|
312 |
-
agent.userData.origin.x + offsetX,
|
313 |
-
agent.userData.origin.y + 5 + Math.sin(gameTime * agent.userData.speed) * 0.5, // Small vertical bob
|
314 |
-
agent.userData.origin.z + offsetZ
|
315 |
-
);
|
316 |
-
}}
|
317 |
-
}}
|
318 |
-
|
319 |
function resetSnake() {{
|
320 |
snake.forEach(seg => scene.remove(seg));
|
321 |
snake = [];
|
322 |
const snakeMaterial = new THREE.MeshStandardMaterial({{color: 0x00ff00}});
|
323 |
-
for (let i
|
324 |
const segment = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 16), snakeMaterial);
|
325 |
segment.position.set(-i * 1.2, 0.5, 0);
|
326 |
segment.castShadow = true;
|
@@ -331,24 +334,23 @@ game_html = f"""
|
|
331 |
lastLength = initialLength;
|
332 |
}}
|
333 |
|
334 |
-
// Controls
|
335 |
const keys = {{w: false, a: false, s: false, d: false, r: false}};
|
336 |
-
|
337 |
const key = event.key.toLowerCase();
|
338 |
if (key === 'w' || key === 'arrowup') keys.w = true;
|
339 |
if (key === 'a' || key === 'arrowleft') keys.a = true;
|
340 |
if (key === 's' || key === 'arrowdown') keys.s = true;
|
341 |
if (key === 'd' || key === 'arrowright') keys.d = true;
|
342 |
if (key === 'r') keys.r = true;
|
343 |
-
}}
|
344 |
-
|
345 |
const key = event.key.toLowerCase();
|
346 |
if (key === 'w' || key === 'arrowup') keys.w = false;
|
347 |
if (key === 'a' || key === 'arrowleft') keys.a = false;
|
348 |
if (key === 's' || key === 'arrowdown') keys.s = false;
|
349 |
if (key === 'd' || key === 'arrowright') keys.d = false;
|
350 |
if (key === 'r' && !gameActive) resetGame();
|
351 |
-
}}
|
352 |
|
353 |
function updateSnake(delta) {{
|
354 |
if (!gameActive) return;
|
@@ -416,7 +418,6 @@ game_html = f"""
|
|
416 |
}}
|
417 |
}}
|
418 |
|
419 |
-
// Update camera to follow snake
|
420 |
const headPos = newHead.position;
|
421 |
camera.position.set(headPos.x, headPos.y + 20, headPos.z + 30);
|
422 |
camera.lookAt(headPos);
|
@@ -442,6 +443,26 @@ game_html = f"""
|
|
442 |
}}
|
443 |
}}
|
444 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
445 |
function loseLife() {{
|
446 |
lives--;
|
447 |
updateUI();
|
@@ -490,75 +511,73 @@ game_html = f"""
|
|
490 |
gameActive = false;
|
491 |
document.getElementById('gameOver').style.display = 'block';
|
492 |
saveScore();
|
|
|
493 |
}}
|
494 |
|
495 |
function updateUI() {{
|
496 |
-
document.getElementById('score').textContent =
|
497 |
-
document.getElementById('time').textContent =
|
498 |
-
document.getElementById('length').textContent =
|
499 |
-
document.getElementById('lives').textContent =
|
500 |
}}
|
501 |
|
502 |
-
let highScores = JSON.parse(localStorage.getItem('highScores')) || [];
|
503 |
function saveScore() {{
|
504 |
highScores.push({{ name: playerName, score: Math.floor(score), time: Math.floor(gameTime) }});
|
505 |
highScores.sort((a, b) => b.score - a.score);
|
506 |
highScores = highScores.slice(0, 5);
|
507 |
localStorage.setItem('highScores', JSON.stringify(highScores));
|
508 |
-
console.log("High Scores:", highScores); // For debugging
|
509 |
}}
|
510 |
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
cycleTime += delta;
|
515 |
-
const cycleDuration = 120;
|
516 |
-
const angle = (cycleTime / cycleDuration) * Math.PI * 2;
|
517 |
-
sunLight.position.set(Math.cos(angle) * 100, Math.sin(angle) * 100, Math.sin(angle) * 50);
|
518 |
-
sunLight.intensity = Math.max(0, Math.sin(angle)) * 0.8;
|
519 |
-
const dayColor = new THREE.Color(0x87CEEB);
|
520 |
-
const nightColor = new THREE.Color(0x001133);
|
521 |
-
scene.background = dayColor.clone().lerp(nightColor, Math.max(0, -Math.sin(angle)));
|
522 |
}}
|
523 |
|
524 |
-
|
525 |
-
|
526 |
-
function animate() {{
|
527 |
-
requestAnimationFrame(animate);
|
528 |
-
const currentTime = performance.now();
|
529 |
-
const delta = (currentTime - lastTime) / 1000;
|
530 |
-
lastTime = currentTime;
|
531 |
-
|
532 |
if (gameActive) {{
|
533 |
gameTime += delta;
|
534 |
if (gameTime >= maxGameTime) endGame();
|
535 |
updateSnake(delta);
|
536 |
updateScore(delta);
|
537 |
updateQuineAgents(delta);
|
538 |
-
updateLighting(delta);
|
539 |
}}
|
540 |
-
|
541 |
renderer.render(scene, camera);
|
542 |
}}
|
543 |
|
544 |
-
|
545 |
-
|
|
|
|
|
546 |
camera.updateProjectionMatrix();
|
547 |
-
renderer.setSize(
|
548 |
-
}}
|
549 |
-
|
550 |
-
|
551 |
-
snake = [];
|
552 |
-
resetSnake();
|
553 |
-
createCity();
|
554 |
-
spawnFood();
|
555 |
-
animate();
|
556 |
</script>
|
557 |
</body>
|
558 |
</html>
|
559 |
"""
|
560 |
|
561 |
-
# Render the HTML
|
562 |
-
html(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
563 |
|
564 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
2 |
+
import streamlit.components.v1 as components
|
3 |
+
|
4 |
+
st.set_page_config(page_title="Galaxian Snake 3D", layout="wide")
|
5 |
+
|
6 |
+
st.title("Galaxian Snake 3D")
|
7 |
+
st.write("Navigate a 3D city as a snake, eating food and avoiding obstacles!")
|
8 |
+
|
9 |
+
# Sliders for container size with initial 3:4 aspect ratio
|
10 |
+
max_width = min(1200, st.session_state.get('window_width', 1200))
|
11 |
+
max_height = min(1600, st.session_state.get('window_height', 1600))
|
12 |
+
|
13 |
+
col1, col2 = st.columns(2)
|
14 |
+
with col1:
|
15 |
+
container_width = st.slider("Container Width (px)", 300, max_width, 768, step=50)
|
16 |
+
with col2:
|
17 |
+
container_height = st.slider("Container Height (px)", 400, max_height, 1024, step=50)
|
18 |
+
|
19 |
+
# Player name input
|
20 |
+
player_name = st.sidebar.text_input("Enter 3-letter name (e.g., ABC):", max_chars=3, value="XYZ").upper()
|
21 |
+
if len(player_name) != 3 or not player_name.isalpha():
|
22 |
+
st.warning("Please enter a valid 3-letter name using A-Z.")
|
23 |
+
player_name = "XYZ"
|
24 |
+
|
25 |
+
html_code = f"""
|
|
|
|
|
26 |
<!DOCTYPE html>
|
27 |
+
<html>
|
28 |
<head>
|
29 |
+
<meta charset="utf-8">
|
|
|
30 |
<title>Galaxian Snake 3D</title>
|
31 |
<style>
|
32 |
+
body {{ margin: 0; overflow: hidden; }}
|
33 |
+
#container {{ width: {container_width}px; height: {container_height}px; margin: 0 auto; }}
|
34 |
+
canvas {{ width: 100%; height: 100%; display: block; }}
|
35 |
+
.ui-panel {{
|
36 |
+
position: absolute;
|
37 |
+
top: 10px;
|
38 |
+
right: 10px;
|
39 |
+
background: rgba(0,0,0,0.7);
|
40 |
+
padding: 15px;
|
41 |
+
border-radius: 5px;
|
42 |
+
color: white;
|
43 |
+
font-family: Arial, sans-serif;
|
44 |
+
z-index: 1000;
|
45 |
}}
|
46 |
+
.ui-panel h3 {{ margin: 0 0 10px; }}
|
47 |
#gameOver {{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: red; font-size: 48px; z-index: 1; display: none; }}
|
48 |
</style>
|
49 |
</head>
|
50 |
<body>
|
51 |
+
<div id="container"></div>
|
52 |
+
<div class="ui-panel">
|
53 |
+
<h3>Leaderboard</h3>
|
54 |
+
<div id="leaderboard"></div>
|
55 |
+
<p>Score: <span id="score">0</span></p>
|
56 |
+
<p>Time: <span id="time">0</span>s</p>
|
57 |
+
<p>Length: <span id="length">3</span></p>
|
58 |
+
<p>Lives: <span id="lives">3</span></p>
|
|
|
|
|
59 |
</div>
|
60 |
<div id="gameOver">Game Over</div>
|
61 |
+
|
62 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
63 |
<script>
|
64 |
+
let scene, camera, renderer, snake, foodItems = [], lSysCreatures = [], quineAgents = [], buildings = [];
|
65 |
let score = 0, gameTime = 0, lives = 3, gameActive = true;
|
|
|
66 |
const playerName = "{player_name}";
|
67 |
const maxGameTime = 300; // 5 minutes in seconds
|
68 |
let initialLength = 3, lastLength = 3, moveCounter = 0, moveInterval = 0.1;
|
69 |
let moveDir = new THREE.Vector3(1, 0, 0);
|
70 |
+
let highScores = JSON.parse(localStorage.getItem('highScores')) || [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
|
72 |
// Building rules
|
73 |
const buildingRules = [
|
|
|
79 |
];
|
80 |
const buildingColors = [0x888888, 0x666666, 0x999999, 0xaaaaaa, 0x555555, 0x334455, 0x445566, 0x223344, 0x556677, 0x667788, 0x993333, 0x884422, 0x553333, 0x772222, 0x664433];
|
81 |
|
82 |
+
function init() {{
|
83 |
+
const container = document.getElementById('container');
|
84 |
+
if (!container) {{
|
85 |
+
console.error('Container not found');
|
86 |
+
return;
|
87 |
+
}}
|
88 |
+
|
89 |
+
// Scene
|
90 |
+
scene = new THREE.Scene();
|
91 |
+
scene.background = new THREE.Color(0x87CEEB);
|
92 |
+
|
93 |
+
// Camera with 3:4 aspect ratio
|
94 |
+
camera = new THREE.PerspectiveCamera(75, 3 / 4, 0.1, 1000);
|
95 |
+
camera.position.set(0, 20, 30);
|
96 |
+
|
97 |
+
// Renderer
|
98 |
+
renderer = new THREE.WebGLRenderer({{ antialias: true }});
|
99 |
+
renderer.setSize({container_width}, {container_height});
|
100 |
+
renderer.shadowMap.enabled = true;
|
101 |
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
102 |
+
container.appendChild(renderer.domElement);
|
103 |
+
|
104 |
+
// Lights
|
105 |
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
|
106 |
+
scene.add(ambientLight);
|
107 |
+
const sunLight = new THREE.DirectionalLight(0xffddaa, 0.8);
|
108 |
+
sunLight.position.set(50, 50, 50);
|
109 |
+
sunLight.castShadow = true;
|
110 |
+
sunLight.shadow.mapSize.width = 1024;
|
111 |
+
sunLight.shadow.mapSize.height = 1024;
|
112 |
+
sunLight.shadow.camera.near = 0.5;
|
113 |
+
sunLight.shadow.camera.far = 500;
|
114 |
+
scene.add(sunLight);
|
115 |
+
|
116 |
+
// Ground
|
117 |
+
const textureLoader = new THREE.TextureLoader();
|
118 |
+
const groundGeometry = new THREE.PlaneGeometry(200, 200);
|
119 |
+
const groundMaterial = new THREE.MeshStandardMaterial({{
|
120 |
+
color: 0x1a5e1a,
|
121 |
+
roughness: 0.8,
|
122 |
+
metalness: 0.2,
|
123 |
+
bumpMap: textureLoader.load('https://threejs.org/examples/textures/terrain/grasslight-big-nm.jpg'),
|
124 |
+
bumpScale: 0.1
|
125 |
+
}});
|
126 |
+
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
127 |
+
ground.rotation.x = -Math.PI / 2;
|
128 |
+
ground.receiveShadow = true;
|
129 |
+
scene.add(ground);
|
130 |
+
|
131 |
+
// Initialize snake
|
132 |
+
resetSnake();
|
133 |
+
createCity();
|
134 |
+
spawnFood();
|
135 |
+
|
136 |
+
// Leaderboard
|
137 |
+
updateLeaderboard();
|
138 |
+
|
139 |
+
// Start timer
|
140 |
+
setInterval(updateGame, 1000 / 60); // 60 FPS
|
141 |
+
|
142 |
+
window.addEventListener('resize', onWindowResize);
|
143 |
+
window.addEventListener('keydown', onKeyDown);
|
144 |
+
window.addEventListener('keyup', onKeyUp);
|
145 |
+
}}
|
146 |
+
|
147 |
function interpretLSystem(rule, position, rotation) {{
|
148 |
let currentString = rule.axiom;
|
149 |
for (let i = 0; i < rule.iterations; i++) {{
|
|
|
163 |
const color = buildingColors[Math.floor(Math.random() * buildingColors.length)];
|
164 |
const material = new THREE.MeshStandardMaterial({{color: color, roughness: 0.7, metalness: 0.2}});
|
165 |
|
166 |
+
for (let char of currentString) {{
|
|
|
167 |
switch (char) {{
|
168 |
case 'F':
|
169 |
const width = rule.baseWidth * (0.5 + Math.random() * 0.5) * scale.x;
|
|
|
302 |
angle: Math.random() * Math.PI * 2,
|
303 |
speed: 0.5 + Math.random() * 0.5,
|
304 |
radius: 3 + Math.random() * 2,
|
305 |
+
ttl: 5
|
306 |
}};
|
307 |
quineAgents.push(agent);
|
308 |
scene.add(agent);
|
309 |
+
setTimeout(() => lSysCreatures.forEach(c => c !== sender && listenToAgent(c)), 1000);
|
310 |
}}
|
311 |
|
312 |
function listenToAgent(creature) {{
|
|
|
319 |
setTimeout(() => scene.remove(response), 2000);
|
320 |
}}
|
321 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
322 |
function resetSnake() {{
|
323 |
snake.forEach(seg => scene.remove(seg));
|
324 |
snake = [];
|
325 |
const snakeMaterial = new THREE.MeshStandardMaterial({{color: 0x00ff00}});
|
326 |
+
for (let i = 0; i < initialLength; i++) {{
|
327 |
const segment = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 16), snakeMaterial);
|
328 |
segment.position.set(-i * 1.2, 0.5, 0);
|
329 |
segment.castShadow = true;
|
|
|
334 |
lastLength = initialLength;
|
335 |
}}
|
336 |
|
|
|
337 |
const keys = {{w: false, a: false, s: false, d: false, r: false}};
|
338 |
+
function onKeyDown(event) {{
|
339 |
const key = event.key.toLowerCase();
|
340 |
if (key === 'w' || key === 'arrowup') keys.w = true;
|
341 |
if (key === 'a' || key === 'arrowleft') keys.a = true;
|
342 |
if (key === 's' || key === 'arrowdown') keys.s = true;
|
343 |
if (key === 'd' || key === 'arrowright') keys.d = true;
|
344 |
if (key === 'r') keys.r = true;
|
345 |
+
}}
|
346 |
+
function onKeyUp(event) {{
|
347 |
const key = event.key.toLowerCase();
|
348 |
if (key === 'w' || key === 'arrowup') keys.w = false;
|
349 |
if (key === 'a' || key === 'arrowleft') keys.a = false;
|
350 |
if (key === 's' || key === 'arrowdown') keys.s = false;
|
351 |
if (key === 'd' || key === 'arrowright') keys.d = false;
|
352 |
if (key === 'r' && !gameActive) resetGame();
|
353 |
+
}}
|
354 |
|
355 |
function updateSnake(delta) {{
|
356 |
if (!gameActive) return;
|
|
|
418 |
}}
|
419 |
}}
|
420 |
|
|
|
421 |
const headPos = newHead.position;
|
422 |
camera.position.set(headPos.x, headPos.y + 20, headPos.z + 30);
|
423 |
camera.lookAt(headPos);
|
|
|
443 |
}}
|
444 |
}}
|
445 |
|
446 |
+
function updateQuineAgents(delta) {{
|
447 |
+
for (let i = quineAgents.length - 1; i >= 0; i--) {{
|
448 |
+
const agent = quineAgents[i];
|
449 |
+
agent.userData.ttl -= delta;
|
450 |
+
if (agent.userData.ttl <= 0) {{
|
451 |
+
scene.remove(agent);
|
452 |
+
quineAgents.splice(i, 1);
|
453 |
+
continue;
|
454 |
+
}}
|
455 |
+
agent.userData.angle += agent.userData.speed * delta;
|
456 |
+
const offsetX = Math.cos(agent.userData.angle) * agent.userData.radius;
|
457 |
+
const offsetZ = Math.sin(agent.userData.angle) * agent.userData.radius;
|
458 |
+
agent.position.set(
|
459 |
+
agent.userData.origin.x + offsetX,
|
460 |
+
agent.userData.origin.y + 5 + Math.sin(gameTime * agent.userData.speed) * 0.5,
|
461 |
+
agent.userData.origin.z + offsetZ
|
462 |
+
);
|
463 |
+
}}
|
464 |
+
}}
|
465 |
+
|
466 |
function loseLife() {{
|
467 |
lives--;
|
468 |
updateUI();
|
|
|
511 |
gameActive = false;
|
512 |
document.getElementById('gameOver').style.display = 'block';
|
513 |
saveScore();
|
514 |
+
updateLeaderboard();
|
515 |
}}
|
516 |
|
517 |
function updateUI() {{
|
518 |
+
document.getElementById('score').textContent = Math.floor(score);
|
519 |
+
document.getElementById('time').textContent = Math.floor(gameTime);
|
520 |
+
document.getElementById('length').textContent = snake.length;
|
521 |
+
document.getElementById('lives').textContent = lives;
|
522 |
}}
|
523 |
|
|
|
524 |
function saveScore() {{
|
525 |
highScores.push({{ name: playerName, score: Math.floor(score), time: Math.floor(gameTime) }});
|
526 |
highScores.sort((a, b) => b.score - a.score);
|
527 |
highScores = highScores.slice(0, 5);
|
528 |
localStorage.setItem('highScores', JSON.stringify(highScores));
|
|
|
529 |
}}
|
530 |
|
531 |
+
function updateLeaderboard() {{
|
532 |
+
const leaderboard = document.getElementById('leaderboard');
|
533 |
+
leaderboard.innerHTML = highScores.map(s => `<p>${{s.name}}: ${{s.score}} (${{s.time}}s)</p>`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
534 |
}}
|
535 |
|
536 |
+
function updateGame() {{
|
537 |
+
const delta = 1 / 60; // Fixed delta for 60 FPS
|
|
|
|
|
|
|
|
|
|
|
|
|
538 |
if (gameActive) {{
|
539 |
gameTime += delta;
|
540 |
if (gameTime >= maxGameTime) endGame();
|
541 |
updateSnake(delta);
|
542 |
updateScore(delta);
|
543 |
updateQuineAgents(delta);
|
|
|
544 |
}}
|
|
|
545 |
renderer.render(scene, camera);
|
546 |
}}
|
547 |
|
548 |
+
function onWindowResize() {{
|
549 |
+
const width = {container_width};
|
550 |
+
const height = {container_height};
|
551 |
+
camera.aspect = 3 / 4;
|
552 |
camera.updateProjectionMatrix();
|
553 |
+
renderer.setSize(width, height);
|
554 |
+
}}
|
555 |
+
|
556 |
+
window.onload = init;
|
|
|
|
|
|
|
|
|
|
|
557 |
</script>
|
558 |
</body>
|
559 |
</html>
|
560 |
"""
|
561 |
|
562 |
+
# Render the HTML component with dynamic size
|
563 |
+
components.html(html_code, width=container_width, height=container_height)
|
564 |
+
|
565 |
+
st.sidebar.title("Galaxian Snake 3D")
|
566 |
+
st.sidebar.write("""
|
567 |
+
## How to Play
|
568 |
+
|
569 |
+
Navigate a snake through a 3D city, eating food and avoiding obstacles.
|
570 |
+
|
571 |
+
### Controls:
|
572 |
+
- **W/A/S/D or Arrow Keys**: Move snake
|
573 |
+
- **R**: Reset after game over
|
574 |
+
- **Sliders**: Adjust play area size
|
575 |
|
576 |
+
### Features:
|
577 |
+
- 3:4 initial play area (768x1024)
|
578 |
+
- Dynamic leaderboard in-game
|
579 |
+
- City with buildings and roads
|
580 |
+
- Quine agents (cyan spheres) orbit creatures
|
581 |
+
- Scoring: 2 pts for doubling length, +2 pts/sec, +4 pts/sec after 10 units, 10 pt bonus at 10+
|
582 |
+
- 5-minute game duration
|
583 |
+
""")
|