Spaces:
Sleeping
Sleeping
import streamlit as st | |
from datetime import datetime | |
# Initialize session state | |
if 'scores' not in st.session_state: | |
st.session_state.scores = [] | |
# HTML/JS game code | |
GAME_HTML = ''' | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<style> | |
body { margin: 0; } | |
#game-container { | |
width: 100%; | |
height: 400px; | |
background: black; | |
position: relative; | |
overflow: hidden; | |
} | |
.lander { | |
width: 32px; | |
height: 32px; | |
position: absolute; | |
transform: translate(-50%, -50%); | |
} | |
.thrust-particle { | |
width: 4px; | |
height: 4px; | |
background: red; | |
position: absolute; | |
border-radius: 50%; | |
} | |
.flag { | |
width: 4px; | |
height: 24px; | |
background: yellow; | |
position: absolute; | |
bottom: 48px; | |
} | |
.hud { | |
position: absolute; | |
color: white; | |
padding: 16px; | |
font-family: sans-serif; | |
} | |
.controls { | |
position: absolute; | |
top: 16px; | |
right: 16px; | |
color: white; | |
font-family: sans-serif; | |
font-size: 14px; | |
} | |
.game-over { | |
position: absolute; | |
inset: 0; | |
background: rgba(0,0,0,0.7); | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
color: white; | |
font-family: sans-serif; | |
text-align: center; | |
} | |
button { | |
background: #a855f7; | |
border: none; | |
color: white; | |
padding: 8px 16px; | |
margin-top: 16px; | |
cursor: pointer; | |
border-radius: 4px; | |
} | |
button:hover { | |
background: #9333ea; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="game-container"> | |
<div class="hud"> | |
<div id="fuel">Fuel: 100%</div> | |
<div id="velocity">Velocity: 0 m/s</div> | |
<div id="score"></div> | |
</div> | |
<div class="controls"> | |
<div>β - Thrust</div> | |
<div>β β - Move</div> | |
<div>Land between flags</div> | |
</div> | |
</div> | |
<script> | |
class LunarLander { | |
constructor() { | |
this.container = document.getElementById('game-container'); | |
this.position = { x: 200, y: 50 }; | |
this.velocity = { x: 0, y: 0 }; | |
this.fuel = 100; | |
this.gameState = 'playing'; | |
this.thrust = false; | |
this.GRAVITY = 0.05; | |
this.THRUST = 0.15; | |
this.LANDING_SPEED = 3; | |
this.groundPoints = [ | |
{x: 0, y: 380}, {x: 100, y: 360}, {x: 150, y: 370}, | |
{x: 200, y: 350}, {x: 300, y: 350}, {x: 350, y: 370}, | |
{x: 400, y: 360}, {x: 450, y: 380}, {x: 500, y: 370} | |
]; | |
this.setupGame(); | |
} | |
setupGame() { | |
this.createLander(); | |
this.createFlags(); | |
this.createGround(); | |
this.setupControls(); | |
this.gameLoop(); | |
} | |
createLander() { | |
this.lander = document.createElement('div'); | |
this.lander.className = 'lander'; | |
this.lander.style.background = '#a855f7'; | |
this.container.appendChild(this.lander); | |
} | |
createFlags() { | |
const flag1 = document.createElement('div'); | |
flag1.className = 'flag'; | |
flag1.style.left = '190px'; | |
const flag2 = document.createElement('div'); | |
flag2.className = 'flag'; | |
flag2.style.left = '310px'; | |
this.container.appendChild(flag1); | |
this.container.appendChild(flag2); | |
} | |
createGround() { | |
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | |
svg.style.position = 'absolute'; | |
svg.style.bottom = '0'; | |
svg.style.width = '100%'; | |
svg.style.height = '100%'; | |
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); | |
const d = `M${this.groundPoints.map(p => `${p.x} ${p.y}`).join(' L')}`; | |
path.setAttribute('d', d); | |
path.setAttribute('stroke', 'white'); | |
path.setAttribute('fill', 'none'); | |
path.setAttribute('stroke-width', '2'); | |
svg.appendChild(path); | |
this.container.appendChild(svg); | |
} | |
setupControls() { | |
document.addEventListener('keydown', (e) => { | |
if (this.gameState !== 'playing' || this.fuel <= 0) return; | |
switch (e.key) { | |
case 'ArrowUp': | |
this.thrust = true; | |
this.velocity.y -= this.THRUST; | |
this.fuel = Math.max(0, this.fuel - 0.5); | |
this.updateThrust(); | |
break; | |
case 'ArrowLeft': | |
this.velocity.x -= 0.1; | |
this.fuel = Math.max(0, this.fuel - 0.2); | |
break; | |
case 'ArrowRight': | |
this.velocity.x += 0.1; | |
this.fuel = Math.max(0, this.fuel - 0.2); | |
break; | |
} | |
}); | |
document.addEventListener('keyup', (e) => { | |
if (e.key === 'ArrowUp') { | |
this.thrust = false; | |
this.updateThrust(); | |
} | |
}); | |
} | |
updateThrust() { | |
const particles = this.lander.querySelectorAll('.thrust-particle'); | |
particles.forEach(p => p.remove()); | |
if (this.thrust) { | |
for (let i = 0; i < 4; i++) { | |
const particle = document.createElement('div'); | |
particle.className = 'thrust-particle'; | |
particle.style.left = `${Math.random() * 8 - 4}px`; | |
particle.style.top = `${Math.random() * 8}px`; | |
this.lander.appendChild(particle); | |
} | |
} | |
} | |
endGame(won) { | |
this.gameState = won ? 'won' : 'crashed'; | |
const score = Math.floor(this.fuel * 100); | |
const overlay = document.createElement('div'); | |
overlay.className = 'game-over'; | |
overlay.innerHTML = ` | |
<div> | |
<h2>${won ? 'Landing Successful!' : 'Crashed!'}</h2> | |
<div>Score: ${score}</div> | |
<button onclick="restartGame()">Play Again</button> | |
</div> | |
`; | |
this.container.appendChild(overlay); | |
if (won) { | |
window.parent.postMessage({ | |
type: 'score', | |
score: score | |
}, '*'); | |
} | |
} | |
checkCollision() { | |
for (let i = 0; i < this.groundPoints.length - 1; i++) { | |
const p1 = this.groundPoints[i]; | |
const p2 = this.groundPoints[i + 1]; | |
if (this.position.x >= p1.x && this.position.x <= p2.x) { | |
const groundY = p1.y + ((p2.y - p1.y) * (this.position.x - p1.x)) / (p2.x - p1.x); | |
if (this.position.y >= groundY - 10) { | |
if (this.velocity.y < this.LANDING_SPEED * 1.5 && | |
Math.abs(this.velocity.x) < 2 && | |
this.position.x >= 190 && | |
this.position.x <= 310) { | |
this.endGame(true); | |
} else { | |
this.endGame(false); | |
} | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
updateHUD() { | |
document.getElementById('fuel').textContent = `Fuel: ${Math.floor(this.fuel)}%`; | |
document.getElementById('velocity').textContent = `Velocity: ${Math.floor(this.velocity.y * 10)} m/s`; | |
} | |
gameLoop = () => { | |
if (this.gameState === 'playing') { | |
this.position.x += this.velocity.x; | |
this.position.y += this.velocity.y; | |
this.velocity.x *= 0.99; | |
this.velocity.y += this.GRAVITY; | |
this.lander.style.left = `${this.position.x}px`; | |
this.lander.style.top = `${this.position.y}px`; | |
this.updateHUD(); | |
this.checkCollision(); | |
} | |
requestAnimationFrame(this.gameLoop); | |
} | |
} | |
let game; | |
function startGame() { | |
game = new LunarLander(); | |
} | |
function restartGame() { | |
document.getElementById('game-container').innerHTML = ` | |
<div class="hud"> | |
<div id="fuel">Fuel: 100%</div> | |
<div id="velocity">Velocity: 0 m/s</div> | |
<div id="score"></div> | |
</div> | |
<div class="controls"> | |
<div>β - Thrust</div> | |
<div>β β - Move</div> | |
<div>Land between flags</div> | |
</div> | |
`; | |
startGame(); | |
} | |
startGame(); | |
</script> | |
</body> | |
</html> | |
''' | |
def main(): | |
st.title("Lunar Lander") | |
# Display game | |
st.components.v1.html(GAME_HTML, height=450) | |
# Handle score updates | |
st.markdown(""" | |
<script> | |
window.addEventListener('message', function(e) { | |
if (e.data.type === 'score') { | |
window.Streamlit.setComponentValue(e.data.score); | |
} | |
}); | |
</script> | |
""", unsafe_allow_html=True) | |
# Display leaderboard | |
if st.session_state.scores: | |
st.subheader("Top Scores") | |
for score in sorted(st.session_state.scores, key=lambda x: x['score'], reverse=True)[:10]: | |
st.write(f"Score: {score['score']} - {score['timestamp'].strftime('%H:%M:%S')}") | |
# Reset scores button | |
if st.button("Reset Scores"): | |
st.session_state.scores = [] | |
st.experimental_rerun() | |
if __name__ == "__main__": | |
main() |