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