awacke1's picture
Update app.py
f41e68b verified
raw
history blame
18 kB
import streamlit as st
from streamlit.components.v1 import html
# Set Streamlit to wide mode
st.set_page_config(layout="wide", page_title="Galaxian Snake 3D")
# Player name input in Streamlit
with st.sidebar:
st.title("Galaxian Snake 3D")
player_name = st.text_input("Enter 3-letter name (e.g., ABC):", max_chars=3, value="XYZ").upper()
if len(player_name) != 3 or not player_name.isalpha():
st.warning("Please enter a valid 3-letter name using A-Z.")
player_name = "XYZ" # Default if invalid
st.write("**Controls:**")
st.write("- WASD or Arrow Keys to move")
st.write("- R to reset after game over")
st.write("**Objective:**")
st.write("- Eat food to grow and score")
st.write("- Avoid obstacles and creatures")
st.write("**Scoring:**")
st.write("- 2 pts for doubling length")
st.write("- +2 pts/sec, +4 pts/sec after 10 units")
st.write("- 10 pt bonus at 10+ units")
st.write("- Game ends at 5 minutes")
# Define the enhanced HTML content with Three.js, injecting player_name
game_html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Galaxian Snake 3D</title>
<style>
html, body {{ margin: 0; padding: 0; overflow: hidden; background: #000; font-family: Arial; height: 100%; width: 100%; }}
canvas {{ display: block; width: 100vw !important; height: 100vh !important; }}
#ui {{ position: absolute; top: 10px; left: 10px; color: white; z-index: 1; }}
#sidebar {{ position: absolute; top: 10px; right: 10px; color: white; width: 200px; background: rgba(0,0,0,0.7); padding: 10px; z-index: 1; }}
#lives {{ position: absolute; top: 40px; left: 10px; color: white; z-index: 1; }}
.message {{ position: absolute; color: cyan; font-size: 16px; z-index: 1; }}
#gameOver {{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: red; font-size: 48px; z-index: 1; display: none; }}
</style>
</head>
<body>
<div id="ui">Score: <span id="score">0</span> | Time: <span id="timer">0</span>s | Length: <span id="length">3</span></div>
<div id="lives">Lives: <span id="livesCount">3</span></div>
<div id="sidebar">
<h3>High Scores</h3>
<div id="highScores"></div>
</div>
<div id="gameOver">Game Over</div>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
let scene, camera, renderer, snake, foodItems = [], lSysCreatures = [], messages = [], cityscape = [];
let clock = new THREE.Clock();
let moveDir = new THREE.Vector3(1, 0, 0), moveSpeed = 2;
let score = 0, gameTime = 0, lives = 3, moveCounter = 0, moveInterval = 0.1;
let highScores = JSON.parse(localStorage.getItem('highScores')) || [];
let gameOver = false, initialLength = 3, lastLength = 3;
const playerName = "{player_name}";
const maxGameTime = 300; // 5 minutes in seconds
function init() {{
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer({{ antialias: true }});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera.position.set(0, 30, 40);
camera.lookAt(0, 0, 0);
resetSnake();
spawnFood();
spawnCityscape();
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 5);
scene.add(directionalLight);
const starsGeometry = new THREE.BufferGeometry();
const starsMaterial = new THREE.PointsMaterial({{ color: 0xffffff, size: 0.1 }});
const starPositions = new Float32Array(1000 * 3);
for (let i = 0; i < 1000; i++) {{
starPositions[i * 3] = (Math.random() - 0.5) * 100;
starPositions[i * 3 + 1] = (Math.random() - 0.5) * 100;
starPositions[i * 3 + 2] = (Math.random() - 0.5) * 100;
}}
starsGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
scene.add(new THREE.Points(starsGeometry, starsMaterial));
window.addEventListener('keydown', onKeyDown);
window.addEventListener('resize', onWindowResize);
updateHighScoresUI();
animate();
}}
function spawnFood() {{
const foodGeometry = new THREE.DodecahedronGeometry(0.5);
const foodMaterial = new THREE.MeshPhongMaterial({{ color: 0xff00ff }});
for (let i = 0; i < 5; i++) {{
const food = new THREE.Mesh(foodGeometry, foodMaterial);
food.position.set((Math.random() - 0.5) * 40, 0, (Math.random() - 0.5) * 40);
foodItems.push(food);
scene.add(food);
}}
}}
function spawnCityscape() {{
const lSys = {{
axiom: "F",
rules: {{ "F": "F[+F]F[-F][F]" }},
angle: 30,
length: 3,
iterations: 2
}};
const material = new THREE.MeshPhongMaterial({{ color: 0x808080 }});
for (let i = 0; i < 10; i++) {{
let turtleString = lSys.axiom;
for (let j = 0; j < lSys.iterations; j++) {{
turtleString = turtleString.split('').map(c => lSys.rules[c] || c).join('');
}}
const building = new THREE.Group();
let stack = [], pos = new THREE.Vector3((Math.random() - 0.5) * 40, 0, (Math.random() - 0.5) * 40);
let dir = new THREE.Vector3(0, lSys.length, 0);
for (let char of turtleString) {{
if (char === 'F') {{
const height = Math.random() * 2 + 1;
const segment = new THREE.Mesh(
Math.random() > 0.5 ? new THREE.BoxGeometry(1, height, 1) : new THREE.CylinderGeometry(0.5, 0.5, height, 8),
material
);
segment.position.copy(pos).add(dir.clone().multiplyScalar(0.5));
segment.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.clone().normalize());
building.add(segment);
pos.add(dir);
}} else if (char === '+') {{
dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), lSys.angle * Math.PI / 180);
}} else if (char === '-') {{
dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), -lSys.angle * Math.PI / 180);
}} else if (char === '[') {{
stack.push({{ pos: pos.clone(), dir: dir.clone() }});
}} else if (char === ']') {{
const state = stack.pop();
pos = state.pos;
dir = state.dir;
}}
}}
building.position.set(pos.x, 0, pos.z);
cityscape.push(building);
scene.add(building);
if (Math.random() > 0.7) spawnLSysCreature(building.position);
}}
}}
function spawnLSysCreature(position) {{
const lSys = {{
axiom: "F",
rules: {{ "F": "F[+F]F[-F]F" }},
angle: 25,
length: 2,
iterations: 2
}};
const material = new THREE.MeshPhongMaterial({{ color: 0x0000ff }});
let turtleString = lSys.axiom;
for (let j = 0; j < lSys.iterations; j++) {{
turtleString = turtleString.split('').map(c => lSys.rules[c] || c).join('');
}}
const creature = new THREE.Group();
let stack = [], pos = position.clone(), dir = new THREE.Vector3(0, lSys.length, 0);
for (let char of turtleString) {{
if (char === 'F') {{
const segment = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, lSys.length, 8), material);
segment.position.copy(pos).add(dir.clone().multiplyScalar(0.5));
segment.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.clone().normalize());
creature.add(segment);
pos.add(dir);
}} else if (char === '+') {{
dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), lSys.angle * Math.PI / 180);
}} else if (char === '-') {{
dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), -lSys.angle * Math.PI / 180);
}} else if (char === '[') {{
stack.push({{ pos: pos.clone(), dir: dir.clone() }});
}} else if (char === ']') {{
const state = stack.pop();
pos = state.pos;
dir = state.dir;
}}
}}
creature.position.copy(position);
lSysCreatures.push(creature);
scene.add(creature);
sendQuineMessage(creature);
}}
function sendQuineMessage(sender) {{
const quine = "function q(){{console.log('Alive! '+q.toString())}}q()";
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
messageDiv.innerText = 'Quine Msg';
messageDiv.style.left = `${{(sender.position.x + 20) * window.innerWidth / 40}}px`;
messageDiv.style.top = `${{(20 - sender.position.z) * window.innerHeight / 40}}px`;
document.body.appendChild(messageDiv);
messages.push({{ div: messageDiv, ttl: 3 }});
setTimeout(() => lSysCreatures.forEach(c => c !== sender && listenToMessage(c)), 1000);
}}
function listenToMessage(creature) {{
const response = new THREE.Mesh(
new THREE.SphereGeometry(0.3, 16, 16),
new THREE.MeshBasicMaterial({{ color: 0xff0000 }})
);
response.position.copy(creature.position).add(new THREE.Vector3(0, 5, 0));
scene.add(response);
setTimeout(() => scene.remove(response), 2000);
}}
function onKeyDown(event) {{
if (gameOver && event.code === 'KeyR') {{
resetGame();
return;
}}
if (gameOver) return;
switch (event.code) {{
case 'ArrowLeft': case 'KeyA': if (moveDir.x !== 1) moveDir.set(-1, 0, 0); break;
case 'ArrowRight': case 'KeyD': if (moveDir.x !== -1) moveDir.set(1, 0, 0); break;
case 'ArrowUp': case 'KeyW': if (moveDir.z !== 1) moveDir.set(0, 0, -1); break;
case 'ArrowDown': case 'KeyS': if (moveDir.z !== -1) moveDir.set(0, 0, 1); break;
}}
}}
function updateSnake(delta) {{
if (gameOver) return;
moveCounter += delta;
if (moveCounter < moveInterval) return;
moveCounter = 0;
const head = snake[0];
const newHead = new THREE.Mesh(head.geometry, head.material);
newHead.position.copy(head.position).add(moveDir.clone().multiplyScalar(1));
if (Math.abs(newHead.position.x) > 20 || Math.abs(newHead.position.z) > 20) {{
loseLife();
return;
}}
for (let i = 1; i < snake.length; i++) {{
if (newHead.position.distanceTo(snake[i].position) < 0.9) {{
loseLife();
return;
}}
}}
for (let building of cityscape) {{
if (newHead.position.distanceTo(building.position) < 2) {{
loseLife();
return;
}}
}}
snake.unshift(newHead);
scene.add(newHead);
for (let i = foodItems.length - 1; i >= 0; i--) {{
if (newHead.position.distanceTo(foodItems[i].position) < 1) {{
scene.remove(foodItems[i]);
foodItems.splice(i, 1);
spawnFood();
if (Math.random() > 0.8) spawnLSysCreature(newHead.position.clone());
checkLengthBonus();
break;
}} else {{
const tail = snake.pop();
scene.remove(tail);
}}
}}
for (let creature of lSysCreatures) {{
if (newHead.position.distanceTo(creature.position) < 2) {{
loseLife();
return;
}}
}}
updateUI();
}}
function checkLengthBonus() {{
const currentLength = snake.length;
if (currentLength >= 2 * lastLength) {{
score += 2;
lastLength = currentLength;
}}
if (currentLength > 10 && lastLength <= 10) {{
score += 10;
}}
}}
function updateScore(delta) {{
const currentLength = snake.length;
if (currentLength > 10) {{
score += 4 * delta;
}} else {{
score += 2 * delta;
}}
}}
function loseLife() {{
lives--;
updateUI();
if (lives <= 0) {{
endGame();
}} else {{
explodeSnake();
}}
}}
function explodeSnake() {{
const particleGeometry = new THREE.SphereGeometry(0.1, 8, 8);
const particleMaterial = new THREE.MeshBasicMaterial({{ color: 0xff0000 }});
for (let i = 0; i < 20; i++) {{
const particle = new THREE.Mesh(particleGeometry, particleMaterial);
particle.position.copy(snake[0].position);
particle.velocity = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).multiplyScalar(3);
scene.add(particle);
setTimeout(() => scene.remove(particle), 1000);
}}
resetSnake();
}}
function resetSnake() {{
snake.forEach(seg => scene.remove(seg));
snake = [];
const snakeMaterial = new THREE.MeshPhongMaterial({{ color: 0x00ff00, shininess: 100 }});
for (let i = 0; i < initialLength; i++) {{
const segment = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 16), snakeMaterial);
segment.position.set(-i * 1.2, 0, 0);
snake.push(segment);
scene.add(segment);
}}
moveDir.set(1, 0, 0);
lastLength = initialLength;
}}
function resetGame() {{
resetSnake();
foodItems.forEach(f => scene.remove(f));
lSysCreatures.forEach(c => scene.remove(c));
cityscape.forEach(b => scene.remove(b));
foodItems = [];
lSysCreatures = [];
cityscape = [];
spawnFood();
spawnCityscape();
score = 0;
lives = 3;
gameOver = false;
gameTime = 0;
moveCounter = 0;
document.getElementById('gameOver').style.display = 'none';
updateUI();
}}
function endGame() {{
gameOver = true;
document.getElementById('gameOver').style.display = 'block';
saveScore();
}}
function updateUI() {{
document.getElementById('score').innerText = Math.floor(score);
document.getElementById('timer').innerText = Math.floor(gameTime);
document.getElementById('livesCount').innerText = lives;
document.getElementById('length').innerText = snake.length;
}}
function updateHighScoresUI() {{
const scoresDiv = document.getElementById('highScores');
scoresDiv.innerHTML = highScores.map(s => `${{s.name}}: ${{s.score}} (${{s.time}}s)`).join('<br>');
}}
function saveScore() {{
highScores.push({{ name: playerName, score: Math.floor(score), time: Math.floor(gameTime) }});
highScores.sort((a, b) => b.score - a.score);
highScores = highScores.slice(0, 5);
localStorage.setItem('highScores', JSON.stringify(highScores));
updateHighScoresUI();
}}
function onWindowResize() {{
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}}
function animate() {{
requestAnimationFrame(animate);
const delta = clock.getDelta();
gameTime += delta;
if (gameTime >= maxGameTime && !gameOver) {{
endGame();
}}
if (!gameOver) {{
updateSnake(delta);
updateScore(delta);
}}
messages = messages.filter(m => {{
m.ttl -= delta;
if (m.ttl <= 0) document.body.removeChild(m.div);
return m.ttl > 0;
}});
renderer.render(scene, camera);
}}
init();
</script>
</body>
</html>
"""
# Render the HTML game with the injected player name
html(game_html, height=800, width=2000, scrolling=False)
st.write("Note: Requires internet for Three.js to load.")