|
import streamlit as st |
|
|
|
|
|
st.set_page_config(layout="wide", page_title="Galaxian 3D Evolution", initial_sidebar_state="expanded") |
|
|
|
|
|
st.sidebar.markdown("## Galaxian 3D Evolution Controls") |
|
st.sidebar.markdown(""" |
|
- **Controls:** |
|
- **WASD:** Move camera (forward, left, back, right) |
|
- **QE:** Rotate camera up/down |
|
- **Mouse:** Orbit camera (click and drag) |
|
- **Arrow Keys:** Direct snake (Up, Down, Left, Right) |
|
- **R:** Reset game |
|
|
|
- **Features:** |
|
- 3D snake navigates a space grid. |
|
- Eat alien food (👾) to grow and trigger L-system growth. |
|
- Creatures exchange quine messages (hover to see). |
|
- Avoid walls and self-collision. |
|
|
|
Enjoy the 3D Galaxian experience! |
|
""") |
|
|
|
|
|
html_code = r""" |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<title>Galaxian 3D Evolution</title> |
|
<style> |
|
body { margin: 0; padding: 0; overflow: hidden; background: black; } |
|
canvas { display: block; } |
|
</style> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script> |
|
</head> |
|
<body> |
|
<script> |
|
let scene, camera, renderer, controls, snake, food, creatures = [], messages = []; |
|
const gridSize = 20; |
|
const worldSize = 400; |
|
|
|
function init() { |
|
// Scene setup |
|
scene = new THREE.Scene(); |
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); |
|
renderer = new THREE.WebGLRenderer(); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
document.body.appendChild(renderer.domElement); |
|
|
|
// Orbit controls |
|
controls = new THREE.OrbitControls(camera, renderer.domElement); |
|
controls.enableDamping = true; |
|
controls.dampingFactor = 0.05; |
|
|
|
// Lighting |
|
const ambientLight = new THREE.AmbientLight(0x404040); |
|
scene.add(ambientLight); |
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); |
|
directionalLight.position.set(1, 1, 1); |
|
scene.add(directionalLight); |
|
|
|
// Starfield |
|
const starsGeometry = new THREE.BufferGeometry(); |
|
const starsMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 2 }); |
|
const starPositions = new Float32Array(1000 * 3); |
|
for (let i = 0; i < 1000; i++) { |
|
starPositions[i * 3] = (Math.random() - 0.5) * worldSize; |
|
starPositions[i * 3 + 1] = (Math.random() - 0.5) * worldSize; |
|
starPositions[i * 3 + 2] = (Math.random() - 0.5) * worldSize; |
|
} |
|
starsGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3)); |
|
const stars = new THREE.Points(starsGeometry, starsMaterial); |
|
scene.add(stars); |
|
|
|
// Initialize snake |
|
snake = []; |
|
const snakeMaterial = new THREE.MeshPhongMaterial({ color: 0x00ff00 }); |
|
for (let i = 0; i < 3; i++) { |
|
const segment = new THREE.Mesh(new THREE.SphereGeometry(gridSize / 2, 32, 32), snakeMaterial); |
|
segment.position.set(-i * gridSize, 0, 0); |
|
snake.push(segment); |
|
scene.add(segment); |
|
} |
|
snake.direction = new THREE.Vector3(gridSize, 0, 0); |
|
|
|
// Food (alien) |
|
placeFood(); |
|
|
|
// L-system creature |
|
createLSysCreature(); |
|
|
|
// Camera position |
|
camera.position.set(0, 100, 200); |
|
camera.lookAt(0, 0, 0); |
|
|
|
animate(); |
|
} |
|
|
|
function placeFood() { |
|
if (food) scene.remove(food); |
|
const foodGeometry = new THREE.DodecahedronGeometry(gridSize / 2); |
|
const foodMaterial = new THREE.MeshPhongMaterial({ color: 0xff00ff }); |
|
food = new THREE.Mesh(foodGeometry, foodMaterial); |
|
food.position.set( |
|
(Math.floor(Math.random() * (worldSize / gridSize)) - worldSize / (2 * gridSize)) * gridSize, |
|
0, |
|
(Math.floor(Math.random() * (worldSize / gridSize)) - worldSize / (2 * gridSize)) * gridSize |
|
); |
|
scene.add(food); |
|
} |
|
|
|
function createLSysCreature() { |
|
const lSys = { |
|
axiom: "F", |
|
rules: { "F": "F[+F]F[-F]F" }, |
|
angle: 25, |
|
length: gridSize, |
|
iterations: 3 |
|
}; |
|
let turtleString = lSys.axiom; |
|
for (let i = 0; i < lSys.iterations; i++) { |
|
turtleString = applyLSysRules(turtleString, lSys.rules); |
|
} |
|
const creatureMaterial = new THREE.MeshPhongMaterial({ color: 0x0000ff }); |
|
const creature = new THREE.Group(); |
|
let stack = []; |
|
let pos = new THREE.Vector3(worldSize / 4, 0, worldSize / 4); |
|
let dir = new THREE.Vector3(0, lSys.length, 0); |
|
|
|
for (let char of turtleString) { |
|
if (char === "F") { |
|
const segment = new THREE.Mesh(new THREE.CylinderGeometry(2, 2, lSys.length, 16), creatureMaterial); |
|
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; |
|
} |
|
} |
|
scene.add(creature); |
|
creatures.push(creature); |
|
sendQuineMessage(creature); |
|
} |
|
|
|
function applyLSysRules(str, rules) { |
|
return str.split("").map(char => rules[char] || char).join(""); |
|
} |
|
|
|
function sendQuineMessage(sender) { |
|
const quine = "function q(){console.log('Message from creature: '+q.toString())}q()"; |
|
const messageGeometry = new THREE.TextGeometry("Quine Msg", { |
|
font: new THREE.FontLoader().parse({}), // Placeholder: requires font file |
|
size: 10, |
|
height: 2 |
|
}); |
|
const messageMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); |
|
const message = new THREE.Mesh(messageGeometry, messageMaterial); |
|
message.position.copy(sender.position).add(new THREE.Vector3(0, 50, 0)); |
|
scene.add(message); |
|
messages.push({ mesh: message, ttl: 100 }); |
|
setTimeout(() => creatures.forEach(c => c !== sender && listenToMessage(c, quine)), 1000); |
|
} |
|
|
|
function listenToMessage(creature, msg) { |
|
const response = new THREE.Mesh( |
|
new THREE.SphereGeometry(5, 32, 32), |
|
new THREE.MeshBasicMaterial({ color: 0xff0000 }) |
|
); |
|
response.position.copy(creature.position).add(new THREE.Vector3(0, 30, 0)); |
|
scene.add(response); |
|
setTimeout(() => scene.remove(response), 2000); |
|
} |
|
|
|
function updateSnake() { |
|
const head = snake[0]; |
|
const newHead = head.clone(); |
|
newHead.position.add(snake.direction); |
|
|
|
if (Math.abs(newHead.position.x) > worldSize / 2 || Math.abs(newHead.position.z) > worldSize / 2) { |
|
resetGame(); |
|
return; |
|
} |
|
for (let i = 1; i < snake.length; i++) { |
|
if (newHead.position.distanceTo(snake[i].position) < gridSize) { |
|
resetGame(); |
|
return; |
|
} |
|
} |
|
|
|
snake.unshift(newHead); |
|
scene.add(newHead); |
|
if (newHead.position.distanceTo(food.position) < gridSize) { |
|
placeFood(); |
|
createLSysCreature(); |
|
} else { |
|
scene.remove(snake.pop()); |
|
} |
|
} |
|
|
|
function resetGame() { |
|
snake.forEach(seg => scene.remove(seg)); |
|
snake = []; |
|
const snakeMaterial = new THREE.MeshPhongMaterial({ color: 0x00ff00 }); |
|
for (let i = 0; i < 3; i++) { |
|
const segment = new THREE.Mesh(new THREE.SphereGeometry(gridSize / 2, 32, 32), snakeMaterial); |
|
segment.position.set(-i * gridSize, 0, 0); |
|
snake.push(segment); |
|
scene.add(segment); |
|
} |
|
snake.direction = new THREE.Vector3(gridSize, 0, 0); |
|
placeFood(); |
|
} |
|
|
|
let moveCounter = 0; |
|
const moveDelay = 10; |
|
function animate() { |
|
requestAnimationFrame(animate); |
|
controls.update(); |
|
moveCounter++; |
|
if (moveCounter >= moveDelay) { |
|
updateSnake(); |
|
moveCounter = 0; |
|
} |
|
messages = messages.filter(m => { |
|
m.ttl--; |
|
if (m.ttl <= 0) scene.remove(m.mesh); |
|
return m.ttl > 0; |
|
}); |
|
renderer.render(scene, camera); |
|
} |
|
|
|
document.addEventListener("keydown", (e) => { |
|
switch (e.key) { |
|
case "ArrowUp": snake.direction.set(0, 0, -gridSize); break; |
|
case "ArrowDown": snake.direction.set(0, 0, gridSize); break; |
|
case "ArrowLeft": snake.direction.set(-gridSize, 0, 0); break; |
|
case "ArrowRight": snake.direction.set(gridSize, 0, 0); break; |
|
case "r": resetGame(); break; |
|
} |
|
}); |
|
|
|
window.addEventListener("resize", () => { |
|
camera.aspect = window.innerWidth / window.innerHeight; |
|
camera.updateProjectionMatrix(); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
}); |
|
|
|
init(); |
|
</script> |
|
</body> |
|
</html> |
|
""" |
|
|
|
st.components.v1.html(html_code, height=700, scrolling=False) |