|
import streamlit as st |
|
import streamlit.components.v1 as components |
|
|
|
st.set_page_config(page_title="Galaxian Snake 3D", layout="wide") |
|
|
|
st.title("Galaxian Snake 3D") |
|
st.write("Navigate a 3D city as a snake, eating food and avoiding obstacles!") |
|
|
|
|
|
max_width = min(1200, st.session_state.get('window_width', 1200)) |
|
max_height = min(1600, st.session_state.get('window_height', 1600)) |
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
container_width = st.slider("Container Width (px)", 300, max_width, 768, step=50) |
|
with col2: |
|
container_height = st.slider("Container Height (px)", 400, max_height, 1024, step=50) |
|
|
|
|
|
player_name = st.sidebar.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" |
|
|
|
html_code = f""" |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Galaxian Snake 3D</title> |
|
<style> |
|
body {{ margin: 0; overflow: hidden; }} |
|
#container {{ width: {container_width}px; height: {container_height}px; margin: 0 auto; }} |
|
canvas {{ width: 100%; height: 100%; display: block; }} |
|
.ui-panel {{ |
|
position: absolute; |
|
top: 10px; |
|
right: 10px; |
|
background: rgba(0,0,0,0.7); |
|
padding: 15px; |
|
border-radius: 5px; |
|
color: white; |
|
font-family: Arial, sans-serif; |
|
z-index: 1000; |
|
}} |
|
.ui-panel h3 {{ margin: 0 0 10px; }} |
|
#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="container"></div> |
|
<div class="ui-panel"> |
|
<h3>Leaderboard</h3> |
|
<div id="leaderboard"></div> |
|
<p>Score: <span id="score">0</span></p> |
|
<p>Time: <span id="time">0</span>s</p> |
|
<p>Length: <span id="length">3</span></p> |
|
<p>Lives: <span id="lives">3</span></p> |
|
</div> |
|
<div id="gameOver">Game Over</div> |
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
|
<script> |
|
let scene, camera, renderer, snake, foodItems = [], lSysCreatures = [], quineAgents = [], buildings = []; |
|
let score = 0, gameTime = 0, lives = 3, gameActive = true; |
|
const playerName = "{player_name}"; |
|
const maxGameTime = 300; // 5 minutes in seconds |
|
let initialLength = 3, lastLength = 3, moveCounter = 0, moveInterval = 0.1; |
|
let moveDir = new THREE.Vector3(1, 0, 0); |
|
let highScores = JSON.parse(localStorage.getItem('highScores')) || []; |
|
|
|
// Building rules |
|
const buildingRules = [ |
|
{{name: "Colonial", axiom: "A", rules: {{"A": "B[+F][-F]", "B": "F[-C][+C]F", "C": "D[-E][+E]", "D": "F[+F][-F]F", "E": "F[-F][+F]"}}, iterations: 2, baseHeight: 10, baseWidth: 6, baseDepth: 6, angle: Math.PI/6, probability: 0.2}}, |
|
{{name: "Victorian", axiom: "A", rules: {{"A": "B[+C][-C][/D][\\D]", "B": "F[+F][-F][/F][\\F]", "C": "F[++F][--F]", "D": "F[+\\F][-\\F]"}}, iterations: 3, baseHeight: 15, baseWidth: 5, baseDepth: 5, angle: Math.PI/5, probability: 0.15}}, |
|
{{name: "Modern", axiom: "A", rules: {{"A": "B[+B][-B]", "B": "F[/C][\\C]", "C": "F"}}, iterations: 2, baseHeight: 20, baseWidth: 8, baseDepth: 8, angle: Math.PI/2, probability: 0.25}}, |
|
{{name: "Skyscraper", axiom: "A", rules: {{"A": "FB[+C][-C]", "B": "FB", "C": "F[+F][-F]"}}, iterations: 4, baseHeight: 30, baseWidth: 10, baseDepth: 10, angle: Math.PI/8, probability: 0.15}}, |
|
{{name: "Simple", axiom: "F", rules: {{"F": "F[+F][-F]"}}, iterations: 1, baseHeight: 8, baseWidth: 6, baseDepth: 6, angle: Math.PI/4, probability: 0.25}} |
|
]; |
|
const buildingColors = [0x888888, 0x666666, 0x999999, 0xaaaaaa, 0x555555, 0x334455, 0x445566, 0x223344, 0x556677, 0x667788, 0x993333, 0x884422, 0x553333, 0x772222, 0x664433]; |
|
|
|
function init() {{ |
|
const container = document.getElementById('container'); |
|
if (!container) {{ |
|
console.error('Container not found'); |
|
return; |
|
}} |
|
|
|
// Scene |
|
scene = new THREE.Scene(); |
|
scene.background = new THREE.Color(0x87CEEB); |
|
|
|
// Camera with 3:4 aspect ratio |
|
camera = new THREE.PerspectiveCamera(75, 3 / 4, 0.1, 1000); |
|
camera.position.set(0, 20, 30); |
|
|
|
// Renderer |
|
renderer = new THREE.WebGLRenderer({{ antialias: true }}); |
|
renderer.setSize({container_width}, {container_height}); |
|
renderer.shadowMap.enabled = true; |
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap; |
|
container.appendChild(renderer.domElement); |
|
|
|
// Lights |
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.2); |
|
scene.add(ambientLight); |
|
const sunLight = new THREE.DirectionalLight(0xffddaa, 0.8); |
|
sunLight.position.set(50, 50, 50); |
|
sunLight.castShadow = true; |
|
sunLight.shadow.mapSize.width = 1024; |
|
sunLight.shadow.mapSize.height = 1024; |
|
sunLight.shadow.camera.near = 0.5; |
|
sunLight.shadow.camera.far = 500; |
|
scene.add(sunLight); |
|
|
|
// Ground |
|
const textureLoader = new THREE.TextureLoader(); |
|
const groundGeometry = new THREE.PlaneGeometry(200, 200); |
|
const groundMaterial = new THREE.MeshStandardMaterial({{ |
|
color: 0x1a5e1a, |
|
roughness: 0.8, |
|
metalness: 0.2, |
|
bumpMap: textureLoader.load('https://threejs.org/examples/textures/terrain/grasslight-big-nm.jpg'), |
|
bumpScale: 0.1 |
|
}}); |
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial); |
|
ground.rotation.x = -Math.PI / 2; |
|
ground.receiveShadow = true; |
|
scene.add(ground); |
|
|
|
// Initialize snake |
|
resetSnake(); |
|
createCity(); |
|
spawnFood(); |
|
|
|
// Leaderboard |
|
updateLeaderboard(); |
|
|
|
// Start timer |
|
setInterval(updateGame, 1000 / 60); // 60 FPS |
|
|
|
window.addEventListener('resize', onWindowResize); |
|
window.addEventListener('keydown', onKeyDown); |
|
window.addEventListener('keyup', onKeyUp); |
|
}} |
|
|
|
function interpretLSystem(rule, position, rotation) {{ |
|
let currentString = rule.axiom; |
|
for (let i = 0; i < rule.iterations; i++) {{ |
|
let newString = ""; |
|
for (let j = 0; j < currentString.length; j++) {{ |
|
newString += rule.rules[currentString[j]] || currentString[j]; |
|
}} |
|
currentString = newString; |
|
}} |
|
|
|
let group = new THREE.Group(); |
|
group.position.copy(position); |
|
const stack = []; |
|
let currentPosition = new THREE.Vector3(0, 0, 0); |
|
let currentRotation = rotation || new THREE.Euler(); |
|
let scale = new THREE.Vector3(1, 1, 1); |
|
const color = buildingColors[Math.floor(Math.random() * buildingColors.length)]; |
|
const material = new THREE.MeshStandardMaterial({{color: color, roughness: 0.7, metalness: 0.2}}); |
|
|
|
for (let char of currentString) {{ |
|
switch (char) {{ |
|
case 'F': |
|
const width = rule.baseWidth * (0.5 + Math.random() * 0.5) * scale.x; |
|
const height = rule.baseHeight * (0.5 + Math.random() * 0.5) * scale.y; |
|
const depth = rule.baseDepth * (0.5 + Math.random() * 0.5) * scale.z; |
|
const geometry = new THREE.BoxGeometry(width, height, depth); |
|
const part = new THREE.Mesh(geometry, material); |
|
part.position.copy(currentPosition); |
|
part.position.y += height / 2; |
|
part.rotation.copy(currentRotation); |
|
part.castShadow = true; |
|
part.receiveShadow = true; |
|
group.add(part); |
|
const direction = new THREE.Vector3(0, height, 0); |
|
direction.applyEuler(currentRotation); |
|
currentPosition.add(direction); |
|
break; |
|
case '+': currentRotation.y += rule.angle; break; |
|
case '-': currentRotation.y -= rule.angle; break; |
|
case '/': currentRotation.x += rule.angle; break; |
|
case '\\\\': currentRotation.x -= rule.angle; break; |
|
case '^': currentRotation.z += rule.angle; break; |
|
case '&': currentRotation.z -= rule.angle; break; |
|
case '[': stack.push({{position: currentPosition.clone(), rotation: currentRotation.clone(), scale: scale.clone()}}); break; |
|
case ']': if (stack.length > 0) {{ const state = stack.pop(); currentPosition = state.position; currentRotation = state.rotation; scale = state.scale; }} break; |
|
case '>': scale.multiplyScalar(1.2); break; |
|
case '<': scale.multiplyScalar(0.8); break; |
|
}} |
|
}} |
|
return group; |
|
}} |
|
|
|
function createCity() {{ |
|
const citySize = 5, spacing = 15; |
|
for (let x = -citySize; x <= citySize; x++) {{ |
|
for (let z = -citySize; z <= citySize; z++) {{ |
|
if (Math.random() < 0.2) continue; |
|
const position = new THREE.Vector3(x * spacing + (Math.random() * 2 - 1), 0, z * spacing + (Math.random() * 2 - 1)); |
|
let selectedRule, random = Math.random(), cumulativeProbability = 0; |
|
for (const rule of buildingRules) {{ |
|
cumulativeProbability += rule.probability; |
|
if (random <= cumulativeProbability) {{ selectedRule = rule; break; }} |
|
}} |
|
if (!selectedRule) selectedRule = buildingRules[0]; |
|
const building = interpretLSystem(selectedRule, position, new THREE.Euler()); |
|
scene.add(building); |
|
buildings.push(building); |
|
if (Math.random() > 0.7) spawnLSysCreature(building.position); |
|
}} |
|
}} |
|
const roadWidth = 8, roadColor = 0x333333; |
|
for (let x = -citySize; x <= citySize; x++) {{ |
|
const road = new THREE.Mesh( |
|
new THREE.PlaneGeometry(roadWidth, citySize * 2 * spacing + roadWidth), |
|
new THREE.MeshStandardMaterial({{color: roadColor, roughness: 0.9, metalness: 0.1}}) |
|
); |
|
road.rotation.x = -Math.PI / 2; |
|
road.position.set(x * spacing, 0.01, 0); |
|
scene.add(road); |
|
}} |
|
for (let z = -citySize; z <= citySize; z++) {{ |
|
const road = new THREE.Mesh( |
|
new THREE.PlaneGeometry(citySize * 2 * spacing + roadWidth, roadWidth), |
|
new THREE.MeshStandardMaterial({{color: roadColor, roughness: 0.9, metalness: 0.1}}) |
|
); |
|
road.rotation.x = -Math.PI / 2; |
|
road.position.set(0, 0.01, z * spacing); |
|
scene.add(road); |
|
}} |
|
}} |
|
|
|
function spawnFood() {{ |
|
const citySize = 5, spacing = 15; |
|
for (let i = 0; i < 10; i++) {{ |
|
const food = new THREE.Mesh( |
|
new THREE.BoxGeometry(1, 1, 1), |
|
new THREE.MeshStandardMaterial({{color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5, transparent: true, opacity: 0.8}}) |
|
); |
|
const x = (Math.random() * 2 - 1) * citySize * spacing; |
|
const z = (Math.random() * 2 - 1) * citySize * spacing; |
|
food.position.set(x, 0.5, z); |
|
foodItems.push(food); |
|
scene.add(food); |
|
}} |
|
}} |
|
|
|
function spawnLSysCreature(position) {{ |
|
const lSys = {{ |
|
axiom: "F", |
|
rules: {{"F": "F[+F]F[-F]F"}}, |
|
angle: 25 * Math.PI / 180, |
|
length: 2, |
|
iterations: 2 |
|
}}; |
|
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); |
|
const material = new THREE.MeshStandardMaterial({{color: 0x0000ff}}); |
|
|
|
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); |
|
}} else if (char === '-') {{ |
|
dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), -lSys.angle); |
|
}} 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); |
|
sendQuineAgent(creature); |
|
}} |
|
|
|
function sendQuineAgent(sender) {{ |
|
const agent = new THREE.Mesh( |
|
new THREE.SphereGeometry(0.3, 16, 16), |
|
new THREE.MeshStandardMaterial({{color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 0.5}}) |
|
); |
|
agent.position.copy(sender.position).add(new THREE.Vector3(0, 5, 0)); |
|
agent.userData = {{ |
|
origin: sender.position.clone(), |
|
angle: Math.random() * Math.PI * 2, |
|
speed: 0.5 + Math.random() * 0.5, |
|
radius: 3 + Math.random() * 2, |
|
ttl: 5 |
|
}}; |
|
quineAgents.push(agent); |
|
scene.add(agent); |
|
setTimeout(() => lSysCreatures.forEach(c => c !== sender && listenToAgent(c)), 1000); |
|
}} |
|
|
|
function listenToAgent(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 resetSnake() {{ |
|
snake.forEach(seg => scene.remove(seg)); |
|
snake = []; |
|
const snakeMaterial = new THREE.MeshStandardMaterial({{color: 0x00ff00}}); |
|
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.5, 0); |
|
segment.castShadow = true; |
|
snake.push(segment); |
|
scene.add(segment); |
|
}} |
|
moveDir.set(1, 0, 0); |
|
lastLength = initialLength; |
|
}} |
|
|
|
const keys = {{w: false, a: false, s: false, d: false, r: false}}; |
|
function onKeyDown(event) {{ |
|
const key = event.key.toLowerCase(); |
|
if (key === 'w' || key === 'arrowup') keys.w = true; |
|
if (key === 'a' || key === 'arrowleft') keys.a = true; |
|
if (key === 's' || key === 'arrowdown') keys.s = true; |
|
if (key === 'd' || key === 'arrowright') keys.d = true; |
|
if (key === 'r') keys.r = true; |
|
}} |
|
function onKeyUp(event) {{ |
|
const key = event.key.toLowerCase(); |
|
if (key === 'w' || key === 'arrowup') keys.w = false; |
|
if (key === 'a' || key === 'arrowleft') keys.a = false; |
|
if (key === 's' || key === 'arrowdown') keys.s = false; |
|
if (key === 'd' || key === 'arrowright') keys.d = false; |
|
if (key === 'r' && !gameActive) resetGame(); |
|
}} |
|
|
|
function updateSnake(delta) {{ |
|
if (!gameActive) return; |
|
moveCounter += delta; |
|
if (moveCounter < moveInterval) return; |
|
moveCounter = 0; |
|
|
|
if (keys.w && moveDir.z !== 1) moveDir.set(0, 0, -1); |
|
else if (keys.s && moveDir.z !== -1) moveDir.set(0, 0, 1); |
|
else if (keys.a && moveDir.x !== 1) moveDir.set(-1, 0, 0); |
|
else if (keys.d && moveDir.x !== -1) moveDir.set(1, 0, 0); |
|
|
|
const head = snake[0]; |
|
const newHead = new THREE.Mesh(head.geometry, head.material); |
|
newHead.position.copy(head.position).add(moveDir.clone().multiplyScalar(1)); |
|
newHead.castShadow = true; |
|
|
|
if (Math.abs(newHead.position.x) > 75 || Math.abs(newHead.position.z) > 75) {{ |
|
loseLife(); |
|
return; |
|
}} |
|
|
|
for (let i = 1; i < snake.length; i++) {{ |
|
if (newHead.position.distanceTo(snake[i].position) < 0.9) {{ |
|
loseLife(); |
|
return; |
|
}} |
|
}} |
|
|
|
for (const building of buildings) {{ |
|
building.traverse((child) => {{ |
|
if (child.isMesh) {{ |
|
const buildingBox = new THREE.Box3().setFromObject(child); |
|
const headPos = newHead.position.clone(); |
|
if (headPos.x + 0.5 > buildingBox.min.x && headPos.x - 0.5 < buildingBox.max.x && |
|
headPos.z + 0.5 > buildingBox.min.z && headPos.z - 0.5 < buildingBox.max.z) {{ |
|
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; |
|
}} |
|
}} |
|
|
|
const headPos = newHead.position; |
|
camera.position.set(headPos.x, headPos.y + 20, headPos.z + 30); |
|
camera.lookAt(headPos); |
|
}} |
|
|
|
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 updateQuineAgents(delta) {{ |
|
for (let i = quineAgents.length - 1; i >= 0; i--) {{ |
|
const agent = quineAgents[i]; |
|
agent.userData.ttl -= delta; |
|
if (agent.userData.ttl <= 0) {{ |
|
scene.remove(agent); |
|
quineAgents.splice(i, 1); |
|
continue; |
|
}} |
|
agent.userData.angle += agent.userData.speed * delta; |
|
const offsetX = Math.cos(agent.userData.angle) * agent.userData.radius; |
|
const offsetZ = Math.sin(agent.userData.angle) * agent.userData.radius; |
|
agent.position.set( |
|
agent.userData.origin.x + offsetX, |
|
agent.userData.origin.y + 5 + Math.sin(gameTime * agent.userData.speed) * 0.5, |
|
agent.userData.origin.z + offsetZ |
|
); |
|
}} |
|
}} |
|
|
|
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 resetGame() {{ |
|
resetSnake(); |
|
foodItems.forEach(f => scene.remove(f)); |
|
lSysCreatures.forEach(c => scene.remove(c)); |
|
quineAgents.forEach(a => scene.remove(a)); |
|
buildings.forEach(b => scene.remove(b)); |
|
foodItems = []; |
|
lSysCreatures = []; |
|
quineAgents = []; |
|
buildings = []; |
|
score = 0; |
|
lives = 3; |
|
gameActive = true; |
|
gameTime = 0; |
|
moveCounter = 0; |
|
createCity(); |
|
spawnFood(); |
|
document.getElementById('gameOver').style.display = 'none'; |
|
updateUI(); |
|
}} |
|
|
|
function endGame() {{ |
|
gameActive = false; |
|
document.getElementById('gameOver').style.display = 'block'; |
|
saveScore(); |
|
updateLeaderboard(); |
|
}} |
|
|
|
function updateUI() {{ |
|
document.getElementById('score').textContent = Math.floor(score); |
|
document.getElementById('time').textContent = Math.floor(gameTime); |
|
document.getElementById('length').textContent = snake.length; |
|
document.getElementById('lives').textContent = lives; |
|
}} |
|
|
|
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)); |
|
}} |
|
|
|
function updateLeaderboard() {{ |
|
const leaderboard = document.getElementById('leaderboard'); |
|
leaderboard.innerHTML = highScores.map(s => `<p>${{s.name}}: ${{s.score}} (${{s.time}}s)</p>`).join(''); |
|
}} |
|
|
|
function updateGame() {{ |
|
const delta = 1 / 60; // Fixed delta for 60 FPS |
|
if (gameActive) {{ |
|
gameTime += delta; |
|
if (gameTime >= maxGameTime) endGame(); |
|
updateSnake(delta); |
|
updateScore(delta); |
|
updateQuineAgents(delta); |
|
}} |
|
renderer.render(scene, camera); |
|
}} |
|
|
|
function onWindowResize() {{ |
|
const width = {container_width}; |
|
const height = {container_height}; |
|
camera.aspect = 3 / 4; |
|
camera.updateProjectionMatrix(); |
|
renderer.setSize(width, height); |
|
}} |
|
|
|
window.onload = init; |
|
</script> |
|
</body> |
|
</html> |
|
""" |
|
|
|
|
|
components.html(html_code, width=container_width, height=container_height) |
|
|
|
st.sidebar.title("Galaxian Snake 3D") |
|
st.sidebar.write(""" |
|
## How to Play |
|
|
|
Navigate a snake through a 3D city, eating food and avoiding obstacles. |
|
|
|
### Controls: |
|
- **W/A/S/D or Arrow Keys**: Move snake |
|
- **R**: Reset after game over |
|
- **Sliders**: Adjust play area size |
|
|
|
### Features: |
|
- 3:4 initial play area (768x1024) |
|
- Dynamic leaderboard in-game |
|
- City with buildings and roads |
|
- Quine agents (cyan spheres) orbit creatures |
|
- Scoring: 2 pts for doubling length, +2 pts/sec, +4 pts/sec after 10 units, 10 pt bonus at 10+ |
|
- 5-minute game duration |
|
""") |