| import pygame |
| import numpy as np |
| from flask import Flask, Response, render_template_string |
| from flask_sock import Sock |
| import time |
| import os |
| import cv2 |
| import threading |
| import json |
|
|
| |
| os.environ['SDL_VIDEODRIVER'] = 'dummy' |
| pygame.init() |
|
|
| try: |
| pygame.mixer.init(frequency=44100, size=-16, channels=2) |
| print("✅ Audio mixer initialized") |
| except Exception as e: |
| print(f"⚠️ Audio mixer not available: {e}") |
|
|
| app = Flask(__name__) |
| sock = Sock(app) |
|
|
| class ShaderRenderer: |
| def __init__(self, width=640, height=480): |
| self.width = width |
| self.height = height |
| self.mouse_x = width // 2 |
| self.mouse_y = height // 2 |
| self.start_time = time.time() |
| self.surface = pygame.Surface((width, height)) |
| self.frame_count = 0 |
| self.last_frame_time = time.time() |
| self.fps = 0 |
| self.button_clicked = False |
| self.sound_source = 'none' |
| |
| def set_mouse(self, x, y): |
| self.mouse_x = max(0, min(self.width, x)) |
| self.mouse_y = max(0, min(self.height, y)) |
| |
| def handle_click(self, x, y): |
| button_rect = pygame.Rect(self.width-200, 120, 180, 40) |
| if button_rect.collidepoint(x, y): |
| self.button_clicked = not self.button_clicked |
| return True |
| return False |
| |
| def render_frame(self): |
| t = time.time() - self.start_time |
| |
| |
| self.frame_count += 1 |
| if time.time() - self.last_frame_time > 1.0: |
| self.fps = self.frame_count |
| self.frame_count = 0 |
| self.last_frame_time = time.time() |
| |
| |
| self.surface.fill((20, 20, 30)) |
| font = pygame.font.Font(None, 24) |
| |
| |
| pygame.draw.rect(self.surface, (255, 100, 100), (10, 10, 100, 30)) |
| text = font.render("TOP", True, (255, 255, 255)) |
| self.surface.blit(text, (20, 15)) |
| |
| |
| pygame.draw.rect(self.surface, (100, 255, 100), (10, self.height-40, 100, 30)) |
| text = font.render("BOTTOM", True, (0, 0, 0)) |
| self.surface.blit(text, (20, self.height-35)) |
| |
| |
| current_time = time.time() |
| seconds = int(current_time) % 60 |
| hundredths = int((current_time * 100) % 100) |
| time_str = f"{seconds:02d}.{hundredths:02d}s" |
| clock_text = font.render(time_str, True, (0, 255, 255)) |
| self.surface.blit(clock_text, (self.width-150, 40)) |
| |
| |
| button_rect = pygame.Rect(self.width-200, 120, 180, 40) |
| mouse_over = button_rect.collidepoint(self.mouse_x, self.mouse_y) |
| |
| if self.button_clicked: |
| button_color = (0, 200, 0) |
| elif mouse_over: |
| button_color = (100, 100, 200) |
| else: |
| button_color = (80, 80, 80) |
| |
| pygame.draw.rect(self.surface, button_color, button_rect) |
| pygame.draw.rect(self.surface, (200, 200, 200), button_rect, 2) |
| |
| btn_text = "✅ CLICKED!" if self.button_clicked else "🔘 CLICK ME" |
| text_surf = font.render(btn_text, True, (255, 255, 255)) |
| text_rect = text_surf.get_rect(center=button_rect.center) |
| self.surface.blit(text_surf, text_rect) |
| |
| |
| circle_size = 30 + int(20 * np.sin(t * 2)) |
| if self.sound_source == 'pygame': |
| color = (100, 255, 100) |
| elif self.sound_source == 'browser': |
| color = (100, 100, 255) |
| else: |
| color = (255, 100, 100) |
| |
| pygame.draw.circle(self.surface, color, |
| (self.mouse_x, self.mouse_y), circle_size) |
| |
| |
| for x in range(0, self.width, 50): |
| alpha = int(40 + 20 * np.sin(x * 0.1 + t)) |
| pygame.draw.line(self.surface, (alpha, alpha, 50), |
| (x, 0), (x, self.height)) |
| for y in range(0, self.height, 50): |
| alpha = int(40 + 20 * np.cos(y * 0.1 + t)) |
| pygame.draw.line(self.surface, (alpha, alpha, 50), |
| (0, y), (self.width, y)) |
| |
| |
| fps_text = font.render(f"FPS: {self.fps}", True, (255, 255, 0)) |
| self.surface.blit(fps_text, (self.width-150, self.height-60)) |
| |
| return pygame.image.tostring(self.surface, 'RGB') |
| |
| def get_frame_jpeg(self, quality=70): |
| frame = self.render_frame() |
| img = np.frombuffer(frame, dtype=np.uint8).reshape((self.height, self.width, 3)) |
| img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) |
| _, jpeg = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, quality]) |
| return jpeg.tobytes() |
|
|
| renderer = ShaderRenderer() |
|
|
| |
| @sock.route('/ws') |
| def websocket(ws): |
| """Single WebSocket connection for all interaction""" |
| while True: |
| try: |
| message = ws.receive() |
| if not message: |
| continue |
| |
| data = json.loads(message) |
| |
| if data['type'] == 'mouse': |
| renderer.set_mouse(data['x'], data['y']) |
| |
| elif data['type'] == 'click': |
| renderer.handle_click(data['x'], data['y']) |
| |
| |
| elif data['type'] == 'sound': |
| renderer.sound_source = data['source'] |
| |
| except: |
| break |
|
|
| |
| @app.route('/video.mjpeg') |
| def video_mjpeg(): |
| def generate(): |
| while True: |
| frame = renderer.get_frame_jpeg() |
| yield (b'--frame\r\n' |
| b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') |
| time.sleep(1/25) |
| |
| return Response( |
| generate(), |
| mimetype='multipart/x-mixed-replace; boundary=frame' |
| ) |
|
|
| @app.route('/') |
| def index(): |
| return render_template_string(''' |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>🎮 Pygame + WebSocket</title> |
| <style> |
| body { |
| margin: 0; |
| background: #0a0a0a; |
| color: white; |
| font-family: 'Segoe UI', Arial, sans-serif; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| min-height: 100vh; |
| } |
| .container { |
| max-width: 900px; |
| padding: 20px; |
| text-align: center; |
| } |
| h1 { color: #4CAF50; } |
| .video-container { |
| background: #000; |
| border-radius: 12px; |
| padding: 5px; |
| margin: 20px 0; |
| position: relative; |
| } |
| #mjpegImg { |
| width: 100%; |
| max-width: 640px; |
| height: auto; |
| border-radius: 8px; |
| cursor: crosshair; |
| } |
| .mouse-coords { |
| position: absolute; |
| bottom: 10px; |
| left: 10px; |
| background: rgba(0,0,0,0.7); |
| color: #4CAF50; |
| padding: 5px 10px; |
| border-radius: 20px; |
| font-family: monospace; |
| } |
| .controls { |
| background: #1a1a1a; |
| border-radius: 12px; |
| padding: 20px; |
| margin-top: 20px; |
| } |
| .sound-buttons { |
| display: flex; |
| gap: 10px; |
| justify-content: center; |
| margin: 20px 0; |
| } |
| button { |
| background: #333; |
| color: white; |
| border: none; |
| padding: 12px 24px; |
| border-radius: 8px; |
| cursor: pointer; |
| font-weight: bold; |
| } |
| button.active { |
| background: #4CAF50; |
| box-shadow: 0 0 20px #4CAF50; |
| } |
| .status { |
| display: flex; |
| justify-content: space-around; |
| margin-top: 15px; |
| padding: 10px; |
| background: #222; |
| border-radius: 8px; |
| } |
| .badge { |
| padding: 5px 10px; |
| border-radius: 20px; |
| background: #333; |
| } |
| .badge.green { background: #4CAF50; } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>🎮 Pygame + WebSocket (Zero API)</h1> |
| |
| <div class="video-container"> |
| <img id="mjpegImg" src="/video.mjpeg" crossorigin="anonymous"> |
| <div id="mouseCoords" class="mouse-coords">X: 320, Y: 240</div> |
| </div> |
| |
| <div class="controls"> |
| <h3>🔊 Sound Source</h3> |
| <div class="sound-buttons"> |
| <button id="btnNone" onclick="setSound('none')" class="active">🔇 None</button> |
| <button id="btnPygame" onclick="setSound('pygame')">🎮 Pygame</button> |
| <button id="btnBrowser" onclick="setSound('browser')">🌐 Browser</button> |
| </div> |
| |
| <div class="status"> |
| <div>Connection: <span id="wsStatus" class="badge green">🟢 Connected</span></div> |
| <div>API Calls: <span class="badge">0</span></div> |
| </div> |
| |
| <p style="color: #666; font-size: 12px; margin-top: 15px;"> |
| ⚡ Zero API polling • All interaction via WebSocket • MJPEG stream |
| </p> |
| </div> |
| </div> |
| |
| <audio id="browserAudio" loop style="display:none;"> |
| <source src="/static/sound.mp3" type="audio/mpeg"> |
| </audio> |
| |
| <script> |
| const img = document.getElementById('mjpegImg'); |
| const browserAudio = document.getElementById('browserAudio'); |
| |
| // WebSocket connection (single socket for everything) |
| const ws = new WebSocket((location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + window.location.host + '/ws'); |
| |
| ws.onopen = () => document.getElementById('wsStatus').innerHTML = '🟢 Connected'; |
| ws.onclose = () => document.getElementById('wsStatus').innerHTML = '🔴 Disconnected'; |
| |
| // Mouse tracking with proper throttling |
| let lastMouseSend = 0; |
| const MOUSE_THROTTLE_MS = 33; // 30fps (change to 50 for 20fps) |
| |
| img.addEventListener('mousemove', (e) => { |
| const rect = img.getBoundingClientRect(); |
| const x = Math.round((e.clientX - rect.left) * 640 / rect.width); |
| const y = Math.round((e.clientY - rect.top) * 480 / rect.height); |
| |
| // Update display immediately (no lag) |
| document.getElementById('mouseCoords').innerHTML = `X: ${x}, Y: ${y}`; |
| |
| // Throttle sends to 30fps |
| const now = Date.now(); |
| if (now - lastMouseSend >= MOUSE_THROTTLE_MS) { |
| ws.send(JSON.stringify({ |
| type: 'mouse', |
| x: x, |
| y: y |
| })); |
| lastMouseSend = now; |
| } |
| }); |
| |
| // Click handling - send via WebSocket |
| img.addEventListener('click', (e) => { |
| const rect = img.getBoundingClientRect(); |
| const x = Math.round((e.clientX - rect.left) * 640 / rect.width); |
| const y = Math.round((e.clientY - rect.top) * 480 / rect.height); |
| |
| ws.send(JSON.stringify({ |
| type: 'click', |
| x: x, |
| y: y |
| })); |
| }); |
| |
| // Sound handling |
| function setSound(source) { |
| document.getElementById('btnNone').className = source === 'none' ? 'active' : ''; |
| document.getElementById('btnPygame').className = source === 'pygame' ? 'active' : ''; |
| document.getElementById('btnBrowser').className = source === 'browser' ? 'active' : ''; |
| |
| if (source === 'browser') { |
| browserAudio.play().catch(e => console.log('Audio error:', e)); |
| } else { |
| browserAudio.pause(); |
| browserAudio.currentTime = 0; |
| } |
| |
| ws.send(JSON.stringify({ |
| type: 'sound', |
| source: source |
| })); |
| } |
| </script> |
| </body> |
| </html> |
| ''') |
|
|
| @app.route('/static/sound.mp3') |
| def serve_sound(): |
| if os.path.exists('sound.mp3'): |
| with open('sound.mp3', 'rb') as f: |
| return Response(f.read(), mimetype='audio/mpeg') |
| return 'Sound not found', 404 |
|
|
| if __name__ == '__main__': |
| port = int(os.environ.get('PORT', 7860)) |
| app.run(host='0.0.0.0', port=port, debug=False, threaded=True) |