|
import gradio as gr |
|
import numpy as np |
|
import time |
|
from PIL import Image, ImageDraw |
|
import random |
|
import json |
|
import base64 |
|
|
|
class SpaceShooterGame: |
|
def __init__(self): |
|
|
|
self.width = 480 |
|
self.height = 640 |
|
|
|
|
|
self.score = 0 |
|
self.lives = 3 |
|
self.game_over = False |
|
self.game_started = False |
|
self.last_update = time.time() |
|
|
|
|
|
self.player = { |
|
'x': self.width / 2, |
|
'y': self.height - 60, |
|
'width': 40, |
|
'height': 40, |
|
'speed': 5, |
|
'color': '#3498db', |
|
'is_moving_left': False, |
|
'is_moving_right': False, |
|
'is_moving_up': False, |
|
'is_moving_down': False, |
|
'is_shooting': False, |
|
'last_shot': 0, |
|
'shoot_cooldown': 300 |
|
} |
|
|
|
|
|
self.enemies = [] |
|
self.bullets = [] |
|
self.stars = [] |
|
self.particles = [] |
|
|
|
|
|
self.last_enemy_spawn = 0 |
|
self.enemy_spawn_rate = 1500 |
|
self.last_star_spawn = 0 |
|
self.star_spawn_rate = 200 |
|
|
|
|
|
self.init_stars() |
|
|
|
def init_stars(self): |
|
for _ in range(50): |
|
self.stars.append({ |
|
'x': random.random() * self.width, |
|
'y': random.random() * self.height, |
|
'size': random.random() * 2 + 1, |
|
'speed': random.random() * 2 + 1 |
|
}) |
|
|
|
def update_stars(self, timestamp): |
|
|
|
for i in range(len(self.stars) - 1, -1, -1): |
|
self.stars[i]['y'] += self.stars[i]['speed'] |
|
|
|
|
|
if self.stars[i]['y'] > self.height: |
|
self.stars.pop(i) |
|
|
|
|
|
if timestamp - self.last_star_spawn > self.star_spawn_rate: |
|
self.stars.append({ |
|
'x': random.random() * self.width, |
|
'y': 0, |
|
'size': random.random() * 2 + 1, |
|
'speed': random.random() * 2 + 1 |
|
}) |
|
self.last_star_spawn = timestamp |
|
|
|
def shoot(self, timestamp): |
|
if self.player['is_shooting'] and timestamp - self.player['last_shot'] > self.player['shoot_cooldown']: |
|
self.bullets.append({ |
|
'x': self.player['x'], |
|
'y': self.player['y'] - self.player['height'] / 2, |
|
'width': 4, |
|
'height': 15, |
|
'speed': 10, |
|
'color': '#ffff00' |
|
}) |
|
self.player['last_shot'] = timestamp |
|
|
|
def update_bullets(self): |
|
for i in range(len(self.bullets) - 1, -1, -1): |
|
self.bullets[i]['y'] -= self.bullets[i]['speed'] |
|
|
|
|
|
if self.bullets[i]['y'] < 0: |
|
self.bullets.pop(i) |
|
|
|
def spawn_enemies(self, timestamp): |
|
if timestamp - self.last_enemy_spawn > self.enemy_spawn_rate: |
|
size = random.random() * 20 + 20 |
|
self.enemies.append({ |
|
'x': random.random() * (self.width - size) + size / 2, |
|
'y': 0, |
|
'width': size, |
|
'height': size, |
|
'speed': random.random() * 2 + 1, |
|
'color': f'hsl({random.random() * 360}, 100%, 50%)' |
|
}) |
|
self.last_enemy_spawn = timestamp |
|
|
|
|
|
if self.enemy_spawn_rate > 500: |
|
self.enemy_spawn_rate -= 10 |
|
|
|
def update_enemies(self): |
|
for i in range(len(self.enemies) - 1, -1, -1): |
|
self.enemies[i]['y'] += self.enemies[i]['speed'] |
|
|
|
|
|
if self.enemies[i]['y'] > self.height: |
|
self.enemies.pop(i) |
|
self.lives -= 1 |
|
|
|
if self.lives <= 0: |
|
self.game_over = True |
|
|
|
def create_explosion(self, x, y, color): |
|
particle_count = 15 |
|
for _ in range(particle_count): |
|
angle = random.random() * 3.14159 * 2 |
|
speed = random.random() * 3 + 1 |
|
self.particles.append({ |
|
'x': x, |
|
'y': y, |
|
'vx': np.cos(angle) * speed, |
|
'vy': np.sin(angle) * speed, |
|
'radius': random.random() * 3 + 1, |
|
'color': color, |
|
'life': 30 |
|
}) |
|
|
|
def update_particles(self): |
|
for i in range(len(self.particles) - 1, -1, -1): |
|
self.particles[i]['x'] += self.particles[i]['vx'] |
|
self.particles[i]['y'] += self.particles[i]['vy'] |
|
self.particles[i]['life'] -= 1 |
|
|
|
if self.particles[i]['life'] <= 0: |
|
self.particles.pop(i) |
|
|
|
def check_collisions(self): |
|
|
|
for i in range(len(self.bullets) - 1, -1, -1): |
|
bullet_removed = False |
|
for j in range(len(self.enemies) - 1, -1, -1): |
|
if ( |
|
self.bullets[i]['x'] < self.enemies[j]['x'] + self.enemies[j]['width'] / 2 and |
|
self.bullets[i]['x'] + self.bullets[i]['width'] > self.enemies[j]['x'] - self.enemies[j]['width'] / 2 and |
|
self.bullets[i]['y'] < self.enemies[j]['y'] + self.enemies[j]['height'] / 2 and |
|
self.bullets[i]['y'] + self.bullets[i]['height'] > self.enemies[j]['y'] - self.enemies[j]['height'] / 2 |
|
): |
|
|
|
self.create_explosion(self.enemies[j]['x'], self.enemies[j]['y'], self.enemies[j]['color']) |
|
self.score += int(self.enemies[j]['width']) |
|
|
|
|
|
if not bullet_removed: |
|
self.bullets.pop(i) |
|
bullet_removed = True |
|
self.enemies.pop(j) |
|
break |
|
|
|
if bullet_removed: |
|
break |
|
|
|
|
|
for i in range(len(self.enemies) - 1, -1, -1): |
|
dx = self.player['x'] - self.enemies[i]['x'] |
|
dy = self.player['y'] - self.enemies[i]['y'] |
|
distance = np.sqrt(dx * dx + dy * dy) |
|
|
|
if distance < (self.player['width'] / 2 + self.enemies[i]['width'] / 2): |
|
|
|
self.create_explosion(self.player['x'], self.player['y'], self.player['color']) |
|
self.create_explosion(self.enemies[i]['x'], self.enemies[i]['y'], self.enemies[i]['color']) |
|
|
|
|
|
self.enemies.pop(i) |
|
|
|
|
|
self.lives -= 1 |
|
|
|
if self.lives <= 0: |
|
self.game_over = True |
|
break |
|
|
|
def update_player(self): |
|
|
|
if self.player['is_moving_left'] and self.player['x'] > self.player['width'] / 2: |
|
self.player['x'] -= self.player['speed'] |
|
if self.player['is_moving_right'] and self.player['x'] < self.width - self.player['width'] / 2: |
|
self.player['x'] += self.player['speed'] |
|
if self.player['is_moving_up'] and self.player['y'] > self.player['height']: |
|
self.player['y'] -= self.player['speed'] |
|
if self.player['is_moving_down'] and self.player['y'] < self.height - self.player['height'] / 2: |
|
self.player['y'] += self.player['speed'] |
|
|
|
def render_frame(self): |
|
|
|
img = Image.new('RGB', (self.width, self.height), (17, 17, 17)) |
|
draw = ImageDraw.Draw(img) |
|
|
|
|
|
for star in self.stars: |
|
draw.ellipse( |
|
[star['x'] - star['size'], star['y'] - star['size'], |
|
star['x'] + star['size'], star['y'] + star['size']], |
|
fill='white' |
|
) |
|
|
|
|
|
for enemy in self.enemies: |
|
|
|
draw.ellipse( |
|
[enemy['x'] - enemy['width'] / 2, enemy['y'] - enemy['height'] / 2, |
|
enemy['x'] + enemy['width'] / 2, enemy['y'] + enemy['height'] / 2], |
|
fill=enemy['color'] |
|
) |
|
|
|
|
|
draw.ellipse( |
|
[enemy['x'] - enemy['width'] / 3, enemy['y'] - enemy['height'] / 3, |
|
enemy['x'] + enemy['width'] / 3, enemy['y'] + enemy['height'] / 3], |
|
outline='white' |
|
) |
|
|
|
|
|
for bullet in self.bullets: |
|
draw.rectangle( |
|
[bullet['x'] - bullet['width'] / 2, bullet['y'], |
|
bullet['x'] + bullet['width'] / 2, bullet['y'] + bullet['height']], |
|
fill=bullet['color'] |
|
) |
|
|
|
|
|
if not self.game_over: |
|
|
|
draw.polygon( |
|
[ |
|
(self.player['x'], self.player['y'] - self.player['height'] / 2), |
|
(self.player['x'] - self.player['width'] / 2, self.player['y'] + self.player['height'] / 2), |
|
(self.player['x'] + self.player['width'] / 2, self.player['y'] + self.player['height'] / 2) |
|
], |
|
fill=self.player['color'] |
|
) |
|
|
|
|
|
draw.polygon( |
|
[ |
|
(self.player['x'] - self.player['width'] / 4, self.player['y'] + self.player['height'] / 2), |
|
(self.player['x'], self.player['y'] + self.player['height'] / 2 + 10), |
|
(self.player['x'] + self.player['width'] / 4, self.player['y'] + self.player['height'] / 2) |
|
], |
|
fill='#ff9900' |
|
) |
|
|
|
|
|
for particle in self.particles: |
|
|
|
alpha = int(255 * (particle['life'] / 30)) |
|
color = self.hex_to_rgb(particle['color']) |
|
particle_color = (color[0], color[1], color[2], alpha) |
|
|
|
draw.ellipse( |
|
[particle['x'] - particle['radius'], particle['y'] - particle['radius'], |
|
particle['x'] + particle['radius'], particle['y'] + particle['radius']], |
|
fill=particle['color'] |
|
) |
|
|
|
|
|
draw.text((10, 10), f"Score: {self.score}", fill='white') |
|
draw.text((self.width - 70, 10), f"Lives: {self.lives}", fill='white') |
|
|
|
|
|
if self.game_over: |
|
|
|
overlay = Image.new('RGBA', (self.width, self.height), (0, 0, 0, 180)) |
|
img = Image.alpha_composite(img.convert('RGBA'), overlay) |
|
draw = ImageDraw.Draw(img) |
|
|
|
|
|
draw.text((self.width // 2 - 40, self.height // 2 - 30), "GAME OVER", fill='red') |
|
draw.text((self.width // 2 - 50, self.height // 2), f"Final Score: {self.score}", fill='white') |
|
draw.text((self.width // 2 - 65, self.height // 2 + 30), "Click to play again", fill='white') |
|
|
|
|
|
if not self.game_started and not self.game_over: |
|
|
|
overlay = Image.new('RGBA', (self.width, self.height), (0, 0, 0, 180)) |
|
img = Image.alpha_composite(img.convert('RGBA'), overlay) |
|
draw = ImageDraw.Draw(img) |
|
|
|
|
|
draw.text((self.width // 2 - 60, self.height // 2 - 50), "SPACE SHOOTER", fill='#ff5555') |
|
draw.text((self.width // 2 - 90, self.height // 2 - 20), "Use arrow keys to move", fill='white') |
|
draw.text((self.width // 2 - 85, self.height // 2), "Click mouse to shoot", fill='white') |
|
draw.text((self.width // 2 - 55, self.height // 2 + 30), "Click to start", fill='white') |
|
|
|
return img |
|
|
|
def update(self): |
|
if not self.game_started or self.game_over: |
|
return self.render_frame() |
|
|
|
current_time = time.time() * 1000 |
|
|
|
|
|
delta_time = current_time - self.last_update |
|
self.last_update = current_time |
|
|
|
|
|
self.update_stars(current_time) |
|
self.spawn_enemies(current_time) |
|
self.update_enemies() |
|
self.update_player() |
|
self.shoot(current_time) |
|
self.update_bullets() |
|
self.update_particles() |
|
self.check_collisions() |
|
|
|
return self.render_frame() |
|
|
|
def handle_click(self, evt: gr.SelectData): |
|
|
|
if not self.game_started: |
|
self.game_started = True |
|
return self.update() |
|
|
|
|
|
if self.game_over: |
|
self.reset_game() |
|
return self.update() |
|
|
|
|
|
if evt.index == 1: |
|
self.player['is_shooting'] = True |
|
self.shoot(time.time() * 1000) |
|
self.player['is_shooting'] = False |
|
|
|
return self.update() |
|
|
|
def handle_keypress(self, key): |
|
if not self.game_started or self.game_over: |
|
self.game_started = True |
|
if self.game_over: |
|
self.reset_game() |
|
return self.update() |
|
|
|
|
|
if key == "ArrowLeft": |
|
self.player['is_moving_left'] = True |
|
elif key == "ArrowRight": |
|
self.player['is_moving_right'] = True |
|
elif key == "ArrowUp": |
|
self.player['is_moving_up'] = True |
|
elif key == "ArrowDown": |
|
self.player['is_moving_down'] = True |
|
elif key == "a": |
|
self.player['is_moving_left'] = True |
|
elif key == "d": |
|
self.player['is_moving_right'] = True |
|
elif key == "w": |
|
self.player['is_moving_up'] = True |
|
elif key == "s": |
|
self.player['is_moving_down'] = True |
|
elif key == " ": |
|
self.player['is_shooting'] = True |
|
|
|
return self.update() |
|
|
|
def handle_keyrelease(self, key): |
|
if key == "ArrowLeft": |
|
self.player['is_moving_left'] = False |
|
elif key == "ArrowRight": |
|
self.player['is_moving_right'] = False |
|
elif key == "ArrowUp": |
|
self.player['is_moving_up'] = False |
|
elif key == "ArrowDown": |
|
self.player['is_moving_down'] = False |
|
elif key == "a": |
|
self.player['is_moving_left'] = False |
|
elif key == "d": |
|
self.player['is_moving_right'] = False |
|
elif key == "w": |
|
self.player['is_moving_up'] = False |
|
elif key == "s": |
|
self.player['is_moving_down'] = False |
|
elif key == " ": |
|
self.player['is_shooting'] = False |
|
|
|
return self.update() |
|
|
|
def handle_mouse_move(self, evt: gr.SelectData): |
|
|
|
if self.game_started and not self.game_over: |
|
x_coordinates = evt.index[0] if isinstance(evt.index, tuple) else evt.index |
|
self.player['x'] = max(self.player['width'] / 2, min(x_coordinates, self.width - self.player['width'] / 2)) |
|
|
|
return self.update() |
|
|
|
def reset_game(self): |
|
|
|
self.score = 0 |
|
self.lives = 3 |
|
self.game_over = False |
|
self.game_started = True |
|
self.enemies = [] |
|
self.bullets = [] |
|
self.particles = [] |
|
self.last_enemy_spawn = 0 |
|
self.enemy_spawn_rate = 1500 |
|
|
|
|
|
self.player['x'] = self.width / 2 |
|
self.player['y'] = self.height - 60 |
|
|
|
self.last_update = time.time() * 1000 |
|
|
|
def hex_to_rgb(self, hex_color): |
|
hex_color = hex_color.lstrip('#') |
|
if len(hex_color) == 6: |
|
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) |
|
else: |
|
|
|
return (255, 0, 0) |
|
|
|
|
|
game = SpaceShooterGame() |
|
|
|
def update_frame(): |
|
return game.update() |
|
|
|
def handle_click(img, evt: gr.SelectData): |
|
return game.handle_click(evt) |
|
|
|
def handle_keypress(key): |
|
return game.handle_keypress(key) |
|
|
|
def handle_keyrelease(key): |
|
return game.handle_keyrelease(key) |
|
|
|
def handle_mouse_move(img, evt: gr.SelectData): |
|
return game.handle_mouse_move(evt) |
|
|
|
|
|
with gr.Blocks() as demo: |
|
gr.Markdown("# Space Shooter Game") |
|
gr.Markdown("Use arrow keys (or WASD) to move. Left click to shoot.") |
|
|
|
with gr.Row(): |
|
game_display = gr.Image(game.render_frame(), elem_id="game-canvas") |
|
|
|
with gr.Row(): |
|
gr.Markdown("### Controls:") |
|
gr.Markdown("- **Arrow Keys or WASD**: Move ship") |
|
gr.Markdown("- **Left Click**: Shoot") |
|
gr.Markdown("- **Click on Game**: Start/Restart") |
|
|
|
|
|
game_display.select(handle_click, [game_display], [game_display]) |
|
|
|
|
|
demo.load(update_frame, [], [game_display], every=0.1) |
|
|
|
|
|
demo.queue() |
|
|
|
|
|
js_keyboard = """ |
|
function setupKeyboardHandlers() { |
|
const gameCanvas = document.getElementById('game-canvas'); |
|
if (!gameCanvas) { |
|
setTimeout(setupKeyboardHandlers, 100); |
|
return; |
|
} |
|
|
|
document.addEventListener('keydown', function(e) { |
|
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'a', 'd', 'w', 's', ' '].includes(e.key)) { |
|
e.preventDefault(); |
|
keyPressEvent(e.key); |
|
} |
|
}); |
|
|
|
document.addEventListener('keyup', function(e) { |
|
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'a', 'd', 'w', 's', ' '].includes(e.key)) { |
|
e.preventDefault(); |
|
keyReleaseEvent(e.key); |
|
} |
|
}); |
|
|
|
// Add mousemove handler with debouncing for better performance |
|
let lastMove = 0; |
|
gameCanvas.addEventListener('mousemove', function(e) { |
|
const now = Date.now(); |
|
if (now - lastMove < 50) return; // Only process every 50ms |
|
lastMove = now; |
|
|
|
const rect = gameCanvas.getBoundingClientRect(); |
|
const x = e.clientX - rect.left; |
|
const y = e.clientY - rect.top; |
|
|
|
// Only update if mouse is over the canvas |
|
if (x >= 0 && x <= rect.width && y >= 0 && y <= rect.height) { |
|
// Scale coordinates to match game dimensions |
|
const scaleX = 480 / rect.width; |
|
const scaledX = x * scaleX; |
|
|
|
mouseMove([scaledX, 0]); |
|
} |
|
}); |
|
} |
|
|
|
// Set up the event handlers once the page loads |
|
setTimeout(setupKeyboardHandlers, 100); |
|
""" |
|
|
|
demo.load(None, [], [], _js=js_keyboard) |
|
|
|
|
|
keypress_event = demo.input(fn=handle_keypress, inputs=[], outputs=[game_display]) |
|
keyrelease_event = demo.input(fn=handle_keyrelease, inputs=[], outputs=[game_display]) |
|
mousemove_event = demo.input(fn=handle_mouse_move, inputs=[game_display], outputs=[game_display]) |
|
|
|
|
|
demo.after_setup(_js=f""" |
|
function keyPressEvent(key) {{ |
|
{keypress_event.name}(key); |
|
}} |
|
|
|
function keyReleaseEvent(key) {{ |
|
{keyrelease_event.name}(key); |
|
}} |
|
|
|
function mouseMove(coords) {{ |
|
{mousemove_event.name}(null, coords); |
|
}} |
|
""") |
|
|
|
|
|
if __name__ == "__main__": |
|
demo.launch() |