|
import streamlit as st |
|
|
|
|
|
st.set_page_config(layout="wide", page_title="Galaxian Snake Game", initial_sidebar_state="expanded") |
|
|
|
|
|
st.sidebar.markdown("## Galaxian Snake Game Controls") |
|
st.sidebar.markdown( |
|
""" |
|
- **Controls:** |
|
- **Q:** Move Up–Left |
|
- **W:** Move Up |
|
- **E:** Move Up–Right |
|
- **A:** Move Left |
|
- **S:** (Center) Continue current direction |
|
- **D:** Move Right |
|
- **Z:** Move Down–Left |
|
- **X:** Move Down |
|
- **C:** Move Down–Right |
|
|
|
- **Rules:** |
|
- The snake moves on a grid and grows when it eats the alien food (👾). |
|
- You must avoid colliding with the walls or yourself. |
|
- Press **R** to restart after a game over. |
|
|
|
Have fun and enjoy the retro Galaxian vibe! |
|
""" |
|
) |
|
|
|
|
|
html_code = r""" |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<title>Galaxian Snake Game</title> |
|
<!-- Include p5.js (with WEBGL disabled because we use 2D grid) --> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js"></script> |
|
<style> |
|
body { margin: 0; padding: 0; overflow: hidden; background: black; } |
|
canvas { display: block; margin: 0 auto; } |
|
</style> |
|
</head> |
|
<body> |
|
<script> |
|
/* |
|
Galaxian Snake Game |
|
- The snake moves on a grid. |
|
- Controls: Q, W, E, A, S, D, Z, X, C (with S meaning “no turn”). |
|
- Food is represented as a retro alien (👾). |
|
- A starfield background evokes classic Galaxian style. |
|
*/ |
|
|
|
// Game configuration |
|
let tileSize = 20; |
|
let cols, rows; |
|
let snake; |
|
let direction; // {x: dx, y: dy} in grid units |
|
let food; |
|
let score; |
|
let gameOver; |
|
let moveCounter = 0; |
|
let moveDelay = 8; // lower => faster snake |
|
|
|
// Starfield for background |
|
let stars = []; |
|
const numStars = 150; |
|
|
|
function setup() { |
|
createCanvas(windowWidth, windowHeight); |
|
frameRate(30); |
|
cols = floor(width / tileSize); |
|
rows = floor(height / tileSize); |
|
resetGame(); |
|
// Create starfield |
|
for (let i = 0; i < numStars; i++) { |
|
stars.push({ |
|
x: random(width), |
|
y: random(height), |
|
speed: random(0.5, 2), |
|
size: random(1, 3) |
|
}); |
|
} |
|
} |
|
|
|
function resetGame() { |
|
snake = []; |
|
// Start snake in the middle with length 3 |
|
let startX = floor(cols / 2); |
|
let startY = floor(rows / 2); |
|
snake.push({x: startX, y: startY}); |
|
snake.push({x: startX - 1, y: startY}); |
|
snake.push({x: startX - 2, y: startY}); |
|
// Initial direction: moving right |
|
direction = {x: 1, y: 0}; |
|
placeFood(); |
|
score = 0; |
|
gameOver = false; |
|
moveCounter = 0; |
|
} |
|
|
|
// Place food (alien) at a random location not occupied by the snake |
|
function placeFood() { |
|
let valid = false; |
|
let newFood; |
|
while (!valid) { |
|
newFood = { |
|
x: floor(random(cols)), |
|
y: floor(random(rows)) |
|
}; |
|
valid = true; |
|
for (let seg of snake) { |
|
if (seg.x === newFood.x && seg.y === newFood.y) { |
|
valid = false; |
|
break; |
|
} |
|
} |
|
} |
|
food = newFood; |
|
} |
|
|
|
function draw() { |
|
// Draw Galaxian–style starfield background |
|
background(0); |
|
noStroke(); |
|
fill(255); |
|
for (let s of stars) { |
|
ellipse(s.x, s.y, s.size); |
|
s.y += s.speed; |
|
if (s.y > height) { |
|
s.y = 0; |
|
s.x = random(width); |
|
} |
|
} |
|
|
|
// Draw grid boundaries (optional: for visual style) |
|
// stroke(40); |
|
// for (let i = 0; i < cols; i++) { |
|
// line(i * tileSize, 0, i * tileSize, height); |
|
// } |
|
// for (let j = 0; j < rows; j++) { |
|
// line(0, j * tileSize, width, j * tileSize); |
|
// } |
|
|
|
// Game update if not over |
|
if (!gameOver) { |
|
moveCounter++; |
|
if (moveCounter >= moveDelay) { |
|
updateSnake(); |
|
moveCounter = 0; |
|
} |
|
} else { |
|
// Display Game Over message |
|
textAlign(CENTER, CENTER); |
|
textSize(64); |
|
fill(255, 50, 50); |
|
text("GAME OVER", width/2, height/2); |
|
textSize(32); |
|
text("Score: " + score + " (Press R to Restart)", width/2, height/2 + 50); |
|
} |
|
|
|
// Draw food as a retro alien emoji |
|
textSize(tileSize); |
|
textAlign(CENTER, CENTER); |
|
text("👾", food.x * tileSize + tileSize/2, food.y * tileSize + tileSize/2); |
|
|
|
// Draw snake as neon blocks |
|
for (let i = 0; i < snake.length; i++) { |
|
let seg = snake[i]; |
|
if (i == 0) { |
|
fill(0, 255, 0); // head bright green |
|
} else { |
|
fill(0, 180, 0); |
|
} |
|
rect(seg.x * tileSize, seg.y * tileSize, tileSize, tileSize); |
|
} |
|
|
|
// Draw score in top-left corner |
|
fill(255); |
|
textSize(16); |
|
textAlign(LEFT, TOP); |
|
text("Score: " + score, 10, 10); |
|
} |
|
|
|
// Update snake position and check collisions |
|
function updateSnake() { |
|
// Determine new head position |
|
let head = snake[0]; |
|
let newHead = { x: head.x + direction.x, y: head.y + direction.y }; |
|
|
|
// Check wall collisions |
|
if (newHead.x < 0 || newHead.x >= cols || newHead.y < 0 || newHead.y >= rows) { |
|
gameOver = true; |
|
return; |
|
} |
|
|
|
// Check self-collision |
|
for (let seg of snake) { |
|
if (seg.x === newHead.x && seg.y === newHead.y) { |
|
gameOver = true; |
|
return; |
|
} |
|
} |
|
|
|
// Add new head |
|
snake.unshift(newHead); |
|
|
|
// Check food collision |
|
if (newHead.x === food.x && newHead.y === food.y) { |
|
score += 10; |
|
placeFood(); |
|
// (Do not remove tail: snake grows) |
|
} else { |
|
// Remove tail (snake moves forward) |
|
snake.pop(); |
|
} |
|
} |
|
|
|
// Map key presses to direction changes using the 3x3 layout: |
|
// Q: (-1,-1), W: (0,-1), E: (1,-1) |
|
// A: (-1, 0), S: (0,0) [no change], D: (1,0) |
|
// Z: (-1, 1), X: (0,1), C: (1,1) |
|
function keyPressed() { |
|
if (gameOver && key.toLowerCase() === 'r') { |
|
resetGame(); |
|
return; |
|
} |
|
|
|
// Only process if game is not over |
|
if (gameOver) return; |
|
|
|
let newDir = null; |
|
let k = key.toLowerCase(); |
|
if (k === 'q') newDir = {x: -1, y: -1}; |
|
else if (k === 'w') newDir = {x: 0, y: -1}; |
|
else if (k === 'e') newDir = {x: 1, y: -1}; |
|
else if (k === 'a') newDir = {x: -1, y: 0}; |
|
else if (k === 's') newDir = {x: 0, y: 0}; // center: no change |
|
else if (k === 'd') newDir = {x: 1, y: 0}; |
|
else if (k === 'z') newDir = {x: -1, y: 1}; |
|
else if (k === 'x') newDir = {x: 0, y: 1}; |
|
else if (k === 'c') newDir = {x: 1, y: 1}; |
|
|
|
// If a valid key was pressed and newDir is not the "no-turn" (0,0) command, |
|
// update direction. Also disallow reversing (only if snake length > 1). |
|
if (newDir) { |
|
if (newDir.x === 0 && newDir.y === 0) { |
|
// "S" was pressed: no turn, so do nothing. |
|
return; |
|
} |
|
if (snake.length > 1) { |
|
// Prevent reversing (i.e., newDir should not be exactly opposite of current) |
|
if (newDir.x === -direction.x && newDir.y === -direction.y) { |
|
return; |
|
} |
|
} |
|
direction = newDir; |
|
} |
|
} |
|
|
|
// Resize canvas on window resize |
|
function windowResized() { |
|
resizeCanvas(windowWidth, windowHeight); |
|
cols = floor(width / tileSize); |
|
rows = floor(height / tileSize); |
|
} |
|
</script> |
|
</body> |
|
</html> |
|
""" |
|
|
|
st.components.v1.html(html_code, height=700, scrolling=False) |
|
|