awacke1 commited on
Commit
5770ddd
·
verified ·
1 Parent(s): 6935cf6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +183 -164
app.py CHANGED
@@ -1,113 +1,73 @@
1
  import streamlit as st
2
- from streamlit.components.v1 import html
3
-
4
- # Set Streamlit to wide mode
5
- st.set_page_config(layout="wide", page_title="Galaxian Snake 3D")
6
-
7
- # Player name input in Streamlit
8
- with st.sidebar:
9
- st.title("Galaxian Snake 3D")
10
- player_name = st.text_input("Enter 3-letter name (e.g., ABC):", max_chars=3, value="XYZ").upper()
11
- if len(player_name) != 3 or not player_name.isalpha():
12
- st.warning("Please enter a valid 3-letter name using A-Z.")
13
- player_name = "XYZ" # Default if invalid
14
- st.write("**Controls:**")
15
- st.write("- WASD or Arrow Keys to move")
16
- st.write("- R to reset after game over")
17
- st.write("**Objective:**")
18
- st.write("- Eat food (yellow cubes) to grow and score")
19
- st.write("- Avoid buildings and creatures")
20
- st.write("**Scoring:**")
21
- st.write("- 2 pts for doubling length")
22
- st.write("- +2 pts/sec, +4 pts/sec after 10 units")
23
- st.write("- 10 pt bonus at 10+ units")
24
- st.write("- Game ends at 5 minutes")
25
-
26
- # Define the HTML content with Three.js, injecting player_name
27
- game_html = f"""
28
  <!DOCTYPE html>
29
- <html lang="en">
30
  <head>
31
- <meta charset="UTF-8">
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; font-family: Arial, sans-serif; background: #000; }}
36
- canvas {{ display: block; }}
37
- .ui-container {{
38
- position: absolute; top: 10px; left: 10px; color: white;
39
- background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
40
- user-select: none;
41
- }}
42
- .controls {{
43
- position: absolute; bottom: 10px; left: 10px; color: white;
44
- background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
 
 
 
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 class="ui-container">
51
- <h2>Galaxian Snake 3D</h2>
52
- <div id="score">Score: 0</div>
53
- <div id="time">Time: 0</div>
54
- <div id="length">Length: 3</div>
55
- <div id="lives">Lives: 3</div>
56
- </div>
57
- <div class="controls">
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
- // Game variables
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 i = 0; i < currentString.length; i++) {{
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 // 5 seconds lifespan
282
  }};
283
  quineAgents.push(agent);
284
  scene.add(agent);
285
- setTimeout(() => lSysCreatures.forEach(c => c !== sender && listenToAgent(c, agent)), 1000);
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 K = 0; i < initialLength; 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
- document.addEventListener('keydown', (event) => {{
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
- document.addEventListener('keyup', (event) => {{
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 = `Score: ${{Math.floor(score)}}`;
497
- document.getElementById('time').textContent = `Time: ${{Math.floor(gameTime)}}`;
498
- document.getElementById('length').textContent = `Length: ${{snake.length}}`;
499
- document.getElementById('lives').textContent = `Lives: ${{lives}}`;
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
- // Lighting cycle
512
- let cycleTime = 0;
513
- function updateLighting(delta) {{
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
- // Game loop
525
- let lastTime = performance.now();
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
- window.addEventListener('resize', () => {{
545
- camera.aspect = window.innerWidth / window.innerHeight;
 
 
546
  camera.updateProjectionMatrix();
547
- renderer.setSize(window.innerWidth, window.innerHeight);
548
- }});
549
-
550
- // Initialize
551
- snake = [];
552
- resetSnake();
553
- createCity();
554
- spawnFood();
555
- animate();
556
  </script>
557
  </body>
558
  </html>
559
  """
560
 
561
- # Render the HTML game with the injected player name
562
- html(game_html, height=800, width=2000, scrolling=False)
 
 
 
 
 
 
 
 
 
 
 
563
 
564
- st.write("Note: Requires internet for Three.js to load.")
 
 
 
 
 
 
 
 
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
+ """)