Spaces:
Sleeping
Sleeping
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.") |