Spaces:
Running
Running
import streamlit as st | |
from streamlit.components.v1 import html | |
import random | |
import string | |
# Set Streamlit to wide mode | |
st.set_page_config(layout="wide") | |
# Define the enhanced HTML content with Three.js game | |
game_html = """ | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Galaxxon - Enhanced Arcade Game</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; } | |
.bonus { position: absolute; color: yellow; font-size: 20px; z-index: 1; } | |
</style> | |
</head> | |
<body> | |
<div id="ui">Score: <span id="score">0</span> | Multiplier: <span id="multiplier">1</span>x | Time: <span id="timer">0</span>s</div> | |
<div id="lives">Lives: <span id="livesCount">5</span></div> | |
<div id="sidebar"> | |
<h3>High Scores</h3> | |
<div id="highScores"></div> | |
<button onclick="saveScore()">Save Score</button> | |
</div> | |
<script type="module"> | |
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js'; | |
let scene, camera, renderer, player, enemies = [], bullets = [], buildings = []; | |
let clock = new THREE.Clock(); | |
let moveLeft = false, moveRight = false, moveUp = false, moveDown = false, shoot = false; | |
let score = 0, multiplier = 1, gameTime = 0, lastHitTime = 0, lives = 5, buildingStreak = 0; | |
let highScores = JSON.parse(localStorage.getItem('highScores')) || []; | |
let exploding = false; | |
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, 5, 15); | |
camera.lookAt(0, 0, -50); | |
const playerGeometry = new THREE.BoxGeometry(1, 1, 1); | |
const playerMaterial = new THREE.MeshPhongMaterial({ color: 0x00ff00, shininess: 100 }); | |
player = new THREE.Mesh(playerGeometry, playerMaterial); | |
player.position.set(0, 0, 0); | |
scene.add(player); | |
spawnBuildings(); | |
spawnEnemyFormations(); | |
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); | |
window.addEventListener('keydown', onKeyDown); | |
window.addEventListener('keyup', onKeyUp); | |
window.addEventListener('resize', onWindowResize); | |
updateHighScoresUI(); | |
animate(); | |
} | |
function spawnBuildings() { | |
const primitives = [ | |
new THREE.BoxGeometry(2, 2, 2), | |
new THREE.CylinderGeometry(1, 1, 3, 8), | |
new THREE.ConeGeometry(1.5, 2, 8) | |
]; | |
const material = new THREE.MeshPhongMaterial({ color: 0x808080, shininess: 50 }); | |
for (let i = 0; i < 10; i++) { | |
const building = new THREE.Group(); | |
const height = Math.random() * 5 + 2; | |
for (let j = 0; j < height; j++) { | |
const primitive = primitives[Math.floor(Math.random() * primitives.length)].clone(); | |
const segment = new THREE.Mesh(primitive, material); | |
segment.position.y = j * 2 - height; | |
building.add(segment); | |
} | |
building.position.set(Math.random() * 40 - 20, height / 2, -50 - Math.random() * 50); | |
building.spawnTimer = 0; | |
buildings.push(building); | |
scene.add(building); | |
} | |
} | |
function spawnEnemyFormations() { | |
const enemyGeometry = new THREE.BoxGeometry(0.8, 0.8, 0.8); | |
const enemyMaterial = new THREE.MeshPhongMaterial({ color: 0xff0000, shininess: 50 }); | |
const formations = [ | |
{ pattern: [[-2, 5], [0, 5], [2, 5], [-1, 6], [1, 6]], center: new THREE.Vector3(0, 5.5, -30) }, | |
{ pattern: [[-3, 4], [-1, 4], [1, 4], [3, 4], [0, 5]], center: new THREE.Vector3(10, 4.5, -40) }, | |
{ pattern: [[-2, 6], [0, 6], [2, 6], [-1, 7], [1, 7]], center: new THREE.Vector3(-10, 6.5, -35) } | |
]; | |
formations.forEach(formation => { | |
formation.pattern.forEach(pos => { | |
const enemy = new THREE.Mesh(enemyGeometry, enemyMaterial); | |
enemy.position.set(formation.center.x + pos[0], formation.center.y + pos[1] - formation.center.y, formation.center.z); | |
enemy.velocity = new THREE.Vector3(); | |
enemy.formationCenter = formation.center; | |
enemy.shootTimer = Math.random() * 5; | |
enemies.push(enemy); | |
scene.add(enemy); | |
}); | |
}); | |
} | |
function spawnEnemyFromPosition(position) { | |
const enemyGeometry = new THREE.BoxGeometry(0.8, 0.8, 0.8); | |
const enemyMaterial = new THREE.MeshPhongMaterial({ color: 0xff0000, shininess: 50 }); | |
const enemy = new THREE.Mesh(enemyGeometry, enemyMaterial); | |
enemy.position.copy(position); | |
enemy.velocity = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, 1).normalize(); | |
enemy.shootTimer = Math.random() * 5; | |
enemies.push(enemy); | |
scene.add(enemy); | |
} | |
function onKeyDown(event) { | |
switch (event.code) { | |
case 'ArrowLeft': case 'KeyA': moveLeft = true; break; | |
case 'ArrowRight': case 'KeyD': moveRight = true; break; | |
case 'ArrowUp': case 'KeyW': moveUp = true; break; | |
case 'ArrowDown': case 'KeyS': moveDown = true; break; | |
case 'Space': shoot = true; break; | |
} | |
} | |
function onKeyUp(event) { | |
switch (event.code) { | |
case 'ArrowLeft': case 'KeyA': moveLeft = false; break; | |
case 'ArrowRight': case 'KeyD': moveRight = false; break; | |
case 'ArrowUp': case 'KeyW': moveUp = false; break; | |
case 'ArrowDown': case 'KeyS': moveDown = false; break; | |
case 'Space': shoot = false; break; | |
} | |
} | |
function updatePlayer(delta) { | |
if (exploding) return; | |
const speed = 10; | |
if (moveLeft && player.position.x > -20) player.position.x -= speed * delta; | |
if (moveRight && player.position.x < 20) player.position.x += speed * delta; | |
if (moveUp && player.position.y < 10) player.position.y += speed * delta; | |
if (moveDown && player.position.y > -5) player.position.y -= speed * delta; | |
if (shoot && clock.getElapsedTime() > 0.2) { | |
shootBullet(); | |
clock = new THREE.Clock(); | |
} | |
} | |
function shootBullet() { | |
const bulletGeometry = new THREE.SphereGeometry(0.2, 8, 8); | |
const bulletMaterial = new THREE.MeshPhongMaterial({ color: 0xffff00, shininess: 100 }); | |
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); | |
bullet.position.copy(player.position); | |
bullet.position.z -= 1; | |
bullet.velocity = new THREE.Vector3(0, 0, -1); | |
bullet.bounces = 0; | |
bullet.timer = 5; | |
bullet.isPlayerBullet = true; | |
bullets.push(bullet); | |
scene.add(bullet); | |
} | |
function shootEnemyBullet(enemy) { | |
const bulletGeometry = new THREE.SphereGeometry(0.2, 8, 8); | |
const bulletMaterial = new THREE.MeshPhongMaterial({ color: 0xff00ff, shininess: 100 }); | |
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); | |
bullet.position.copy(enemy.position); | |
bullet.velocity = new THREE.Vector3(0, 0, 1); | |
bullet.bounces = 0; | |
bullet.timer = 5; | |
bullet.isPlayerBullet = false; | |
bullets.push(bullet); | |
scene.add(bullet); | |
} | |
function updateBullets(delta) { | |
const bulletSpeed = 20; | |
for (let i = bullets.length - 1; i >= 0; i--) { | |
bullets[i].position.add(bullets[i].velocity.clone().multiplyScalar(bulletSpeed * delta)); | |
bullets[i].timer -= delta; | |
if (bullets[i].position.z < -100 || bullets[i].position.z > 10 || bullets[i].timer <= 0) { | |
scene.remove(bullets[i]); | |
bullets.splice(i, 1); | |
continue; | |
} | |
if (bullets[i].position.x < -20 || bullets[i].position.x > 20) { | |
bullets[i].velocity.x *= -1; | |
bullets[i].bounces++; | |
} | |
if (bullets[i].position.y < -5 || bullets[i].position.y > 10) { | |
bullets[i].velocity.y *= -1; | |
bullets[i].bounces++; | |
} | |
if (bullets[i].bounces > 3) { | |
scene.remove(bullets[i]); | |
bullets.splice(i, 1); | |
continue; | |
} | |
for (let j = buildings.length - 1; j >= 0; j--) { | |
if (buildings[j].children.some(child => child.visible && bullets[i].position.distanceTo(buildings[j].position) < 3)) { | |
bullets[i].velocity.z *= -1; | |
bullets[i].bounces++; | |
break; | |
} | |
} | |
checkBulletCollisions(bullets[i], i); | |
} | |
} | |
function checkBulletCollisions(bullet, bulletIndex) { | |
for (let i = enemies.length - 1; i >= 0; i--) { | |
if (bullet.position.distanceTo(enemies[i].position) < 1 && bullet.isPlayerBullet) { | |
scene.remove(enemies[i]); | |
enemies.splice(i, 1); | |
scene.remove(bullet); | |
bullets.splice(bulletIndex, 1); | |
score += 10 * multiplier; | |
if (clock.getElapsedTime() - lastHitTime < 2) multiplier += 0.5; | |
lastHitTime = clock.getElapsedTime(); | |
updateUI(); | |
return; | |
} | |
} | |
if (!bullet.isPlayerBullet && bullet.position.distanceTo(player.position) < 1 && !exploding) { | |
explodePlayer(); | |
scene.remove(bullet); | |
bullets.splice(bulletIndex, 1); | |
return; | |
} | |
for (let j = bullets.length - 1; j >= 0; j--) { | |
if (j !== bulletIndex && bullet.position.distanceTo(bullets[j].position) < 0.4) { | |
scene.remove(bullet); | |
scene.remove(bullets[j]); | |
bullets.splice(Math.max(bulletIndex, j), 1); | |
bullets.splice(Math.min(bulletIndex, j), 1); | |
break; | |
} | |
} | |
} | |
function updateFlockingEnemies(delta) { | |
const speed = 3; | |
enemies.forEach(enemy => { | |
if (enemy.formationCenter) { | |
const centerDir = enemy.formationCenter.clone().sub(enemy.position).normalize(); | |
enemy.velocity.lerp(centerDir, delta * 0.5); | |
enemy.position.add(enemy.velocity.clone().multiplyScalar(delta * speed)); | |
} else { | |
enemy.position.add(enemy.velocity.clone().multiplyScalar(delta * speed)); | |
if (enemy.position.z > 10) { | |
scene.remove(enemy); | |
enemies.splice(enemies.indexOf(enemy), 1); | |
return; | |
} | |
} | |
enemy.shootTimer -= delta; | |
if (enemy.shootTimer <= 0) { | |
shootEnemyBullet(enemy); | |
enemy.shootTimer = Math.random() * 5 + 2; | |
} | |
}); | |
if (enemies.length < 10) spawnEnemyFormations(); | |
} | |
function updateBuildings(delta) { | |
const buildingSpeed = 5; | |
for (let i = buildings.length - 1; i >= 0; i--) { | |
const building = buildings[i]; | |
building.position.z += buildingSpeed * delta; | |
if (building.position.z > 20) { | |
building.position.z = -50 - Math.random() * 50; | |
building.position.x = Math.random() * 40 - 20; | |
building.children.forEach(child => child.visible = true); | |
} | |
const distToPlayer = building.position.distanceTo(player.position); | |
if (distToPlayer < 10 && building.spawnTimer <= 0) { | |
spawnEnemyFromBuilding(building); | |
building.spawnTimer = 2; | |
} | |
building.spawnTimer -= delta; | |
if (!exploding && distToPlayer < 3 && building.children.some(child => child.visible)) { | |
explodePlayer(); | |
destroyBuilding(building, i); | |
} | |
} | |
} | |
function spawnEnemyFromBuilding(building) { | |
const topPos = building.position.clone(); | |
topPos.y += building.children.length * 2; | |
spawnEnemyFromPosition(topPos); | |
} | |
function explodePlayer() { | |
exploding = true; | |
lives--; | |
updateUI(); | |
const particleGeometry = new THREE.SphereGeometry(0.1, 8, 8); | |
const particleMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); | |
for (let i = 0; i < 30; i++) { | |
const particle = new THREE.Mesh(particleGeometry, particleMaterial); | |
particle.position.copy(player.position); | |
particle.velocity = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).multiplyScalar(5); | |
scene.add(particle); | |
setTimeout(() => scene.remove(particle), 1000); | |
} | |
player.visible = false; | |
setTimeout(() => { | |
player.visible = true; | |
exploding = false; | |
player.position.set(0, 0, 0); | |
if (lives === 1) alert("Last life remaining!"); | |
if (lives <= 0) { | |
alert("Game Over! Final Score: " + score); | |
saveScore(); | |
lives = 5; | |
score = 0; | |
multiplier = 1; | |
buildingStreak = 0; | |
updateUI(); | |
} | |
}, 1000); | |
} | |
function destroyBuilding(building, index) { | |
const explosionPos = building.position.clone(); | |
const particleGeometry = new THREE.SphereGeometry(0.2, 8, 8); | |
const particleMaterial = new THREE.MeshBasicMaterial({ color: 0xff8800 }); | |
for (let i = 0; i < 20; i++) { | |
const particle = new THREE.Mesh(particleGeometry, particleMaterial); | |
particle.position.copy(explosionPos); | |
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); | |
} | |
building.children.forEach((segment, idx) => { | |
setTimeout(() => segment.visible = false, idx * 100); | |
}); | |
buildingStreak++; | |
const bonus = 50 * buildingStreak; | |
score += bonus; | |
showBonus(explosionPos, bonus); | |
if (Math.random() < 0.5) { | |
for (let i = 0; i < 2; i++) { | |
setTimeout(() => spawnEnemyFromPosition(explosionPos), Math.random() * 500); | |
} | |
} | |
} | |
function showBonus(position, points) { | |
const bonusDiv = document.createElement('div'); | |
bonusDiv.className = 'bonus'; | |
bonusDiv.innerText = `+${points}`; | |
bonusDiv.style.left = `${(position.x + 20) * window.innerWidth / 40}px`; | |
bonusDiv.style.top = `${(10 - position.y) * window.innerHeight / 15}px`; | |
document.body.appendChild(bonusDiv); | |
setTimeout(() => document.body.removeChild(bonusDiv), 1000); | |
} | |
function updateUI() { | |
document.getElementById('score').innerText = score; | |
document.getElementById('multiplier').innerText = multiplier.toFixed(1); | |
document.getElementById('timer').innerText = Math.floor(gameTime); | |
document.getElementById('livesCount').innerText = lives; | |
if (clock.getElapsedTime() - lastHitTime > 5) multiplier = 1; | |
} | |
function updateHighScoresUI() { | |
const scoresDiv = document.getElementById('highScores'); | |
scoresDiv.innerHTML = highScores.map(s => `${s.name}: ${s.score} (${s.time}s)`).join('<br>'); | |
} | |
window.saveScore = function() { | |
const name = prompt("Enter 3-letter name:", generateRandomName()); | |
if (name && name.length === 3) { | |
highScores.push({ name, 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 generateRandomName() { | |
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; | |
return Array(3).fill().map(() => letters[Math.floor(Math.random() * letters.length)]).join(''); | |
} | |
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; | |
updatePlayer(delta); | |
updateBullets(delta); | |
updateFlockingEnemies(delta); | |
updateBuildings(delta); | |
updateUI(); | |
renderer.render(scene, camera); | |
} | |
init(); | |
</script> | |
</body> | |
</html> | |
""" | |
# Streamlit app with sidebar for title and instructions | |
with st.sidebar: | |
st.title("Galaxxon - Enhanced Arcade Game") | |
st.write("**Controls:**") | |
st.write("- Use WASD or Arrow Keys to move") | |
st.write("- Spacebar to shoot") | |
st.write("**Objective:**") | |
st.write("- Crash into buildings for bonus points") | |
st.write("- Destroy enemies and avoid their bullets") | |
# Render the HTML game full-screen | |
html(game_html, height=1000, width=2000, scrolling=False) | |
st.write("Note: The game runs in your browser. Ensure you have an internet connection for Three.js to load.") | |