jbilcke-hf's picture
jbilcke-hf HF Staff
Upload 76 files
260ff53 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Game Multiverse</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #121212;
color: #e0e0e0;
display: flex;
flex-direction: column;
align-items: center;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
overflow-x: hidden;
}
.container {
width: 100%;
max-width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.game-area {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-height: 85vh;
margin: 0;
position: relative;
}
#mouse-tracking-area {
position: relative;
width: 100%;
height: auto;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
max-height: 85vh;
}
#game-canvas {
width: 100%;
height: auto;
max-height: 85vh;
object-fit: contain;
background-color: #000;
pointer-events: none;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}
.controls {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 1200px;
padding: 10px;
background-color: rgba(0, 0, 0, 0.5);
position: absolute;
bottom: 0;
z-index: 10;
box-sizing: border-box;
}
.panels-container {
display: flex;
width: 100%;
max-width: 1200px;
margin: 10px auto;
gap: 10px;
}
.panel {
flex: 1;
background-color: #1E1E1E;
border-radius: 5px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transition: height 0.3s ease;
}
.panel-header {
background-color: #272727;
padding: 10px 15px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.panel-title {
font-weight: bold;
color: #4CAF50;
}
.toggle-button {
background: none;
border: none;
color: #e0e0e0;
font-size: 18px;
cursor: pointer;
}
.toggle-button:focus {
outline: none;
}
.panel-content {
padding: 15px;
max-height: 300px;
overflow-y: auto;
transition: all 0.3s ease;
}
.collapsed .panel-content {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
overflow: hidden;
}
button {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 15px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
transition: background-color 0.3s;
}
button:hover {
background-color: #45a049;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
select {
padding: 10px;
border-radius: 5px;
background-color: #2A2A2A;
color: #e0e0e0;
border: 1px solid #4CAF50;
}
.status {
margin-top: 10px;
color: #4CAF50;
}
.key-indicators {
display: flex;
justify-content: center;
margin-top: 15px;
}
.key {
width: 40px;
height: 40px;
margin: 0 5px;
background-color: #2A2A2A;
border: 1px solid #444;
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
transition: background-color 0.2s;
}
.key.active {
background-color: #4CAF50;
color: white;
}
.key-row {
display: flex;
justify-content: center;
margin: 5px 0;
}
.spacebar {
width: 150px;
}
.connection-info {
font-family: monospace;
height: 100%;
overflow-y: auto;
}
.log-entry {
margin: 5px 0;
padding: 3px;
border-bottom: 1px solid #333;
}
.fps-counter {
position: absolute;
top: 10px;
right: 10px;
background-color: rgba(0,0,0,0.5);
color: #4CAF50;
padding: 5px;
border-radius: 3px;
font-family: monospace;
z-index: 20;
}
#mouse-position {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0,0,0,0.5);
color: #4CAF50;
padding: 5px;
border-radius: 3px;
font-family: monospace;
z-index: 20;
}
@media (max-width: 768px) {
.panels-container {
flex-direction: column;
}
}
.header {
text-align: center;
padding: 15px;
margin-bottom: 20px;
}
.header h1 {
margin: 0;
color: #4CAF50;
font-size: 2rem;
}
.header p {
margin-top: 5px;
color: #aaa;
}
</style>
</head>
<body>
<div class="header">
<h1>AI Game Multiverse</h1>
<p>Play procedurally generated games using AI</p>
</div>
<div class="container">
<div class="game-area">
<div id="mouse-tracking-area">
<img id="game-canvas" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" alt="Game Frame">
<div id="mouse-position">Mouse: 0.00, 0.00</div>
<div class="fps-counter" id="fps-counter">FPS: 0</div>
</div>
<div class="controls">
<button id="connect-btn">Connect</button>
<button id="start-stream-btn" disabled>Start Stream</button>
<button id="stop-stream-btn" disabled>Stop Stream</button>
<select id="scene-select" disabled>
<option value="forest">Forest</option>
<option value="desert">Desert</option>
<option value="beach">Beach</option>
<option value="hills">Hills</option>
<option value="river">River</option>
<option value="plain">Plain</option>
</select>
</div>
</div>
<div class="panels-container">
<!-- Controls Panel -->
<div class="panel" id="controls-panel">
<div class="panel-header" onclick="togglePanel('controls-panel')">
<div class="panel-title">Keyboard Controls</div>
<button class="toggle-button"></button>
</div>
<div class="panel-content">
<div class="key-indicators">
<div class="key-row">
<div id="key-w" class="key">W</div>
</div>
<div class="key-row">
<div id="key-a" class="key">A</div>
<div id="key-s" class="key">S</div>
<div id="key-d" class="key">D</div>
</div>
<div class="key-row">
<div id="key-space" class="key spacebar">SPACE</div>
</div>
<div class="key-row">
<div id="key-shift" class="key">SHIFT</div>
</div>
</div>
<p class="status">
W or ↑ = Forward, S or ↓ = Back, A or ← = Left, D or → = Right<br>
Space = Jump, Shift = Attack<br>
Click on game view to capture mouse (ESC to release)<br>
Mouse = Look around
</p>
</div>
</div>
<!-- Connection Log Panel -->
<div class="panel" id="log-panel">
<div class="panel-header" onclick="togglePanel('log-panel')">
<div class="panel-title">Connection Log</div>
<button class="toggle-button"></button>
</div>
<div class="panel-content">
<div class="connection-info" id="connection-log">
<div class="log-entry">Welcome to AI Game Multiverse. Click Connect to begin.</div>
</div>
</div>
</div>
</div>
</div>
<script>
// WebSocket connection
let socket = null;
let userId = null;
let isStreaming = false;
let lastFrameTime = 0;
let frameCount = 0;
let fpsUpdateInterval = null;
// DOM Elements
const connectBtn = document.getElementById('connect-btn');
const startStreamBtn = document.getElementById('start-stream-btn');
const stopStreamBtn = document.getElementById('stop-stream-btn');
const sceneSelect = document.getElementById('scene-select');
const gameCanvas = document.getElementById('game-canvas');
const connectionLog = document.getElementById('connection-log');
const mousePosition = document.getElementById('mouse-position');
const fpsCounter = document.getElementById('fps-counter');
const mouseTrackingArea = document.getElementById('mouse-tracking-area');
// Pointer Lock API support check
const pointerLockSupported = 'pointerLockElement' in document ||
'mozPointerLockElement' in document ||
'webkitPointerLockElement' in document;
// Keyboard DOM elements
const keyElements = {
'w': document.getElementById('key-w'),
'a': document.getElementById('key-a'),
's': document.getElementById('key-s'),
'd': document.getElementById('key-d'),
'space': document.getElementById('key-space'),
'shift': document.getElementById('key-shift')
};
// Key mapping to action names
const keyToAction = {
'w': 'forward',
'arrowup': 'forward',
'a': 'left',
'arrowleft': 'left',
's': 'back',
'arrowdown': 'back',
'd': 'right',
'arrowright': 'right',
' ': 'jump',
'shift': 'attack'
};
// Key state tracking
const keyState = {
'forward': false,
'back': false,
'left': false,
'right': false,
'jump': false,
'attack': false
};
// Mouse state
const mouseState = {
x: 0,
y: 0,
captured: false
};
// Test server connectivity before establishing WebSocket
async function testServerConnectivity() {
try {
// Get base path by extracting path from the URL
const basePath = window.location.pathname.replace(/\/+$/, '');
// Try to fetch the debug endpoint to see if the server is accessible
const response = await fetch(`${window.location.protocol}//${window.location.host}${basePath}/api/debug`);
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
const debugInfo = await response.json();
logMessage(`Server connection test successful! Server time: ${new Date(debugInfo.server_time * 1000).toLocaleTimeString()}`);
// Log available routes from server
if (debugInfo.all_routes && debugInfo.all_routes.length > 0) {
logMessage(`Available routes: ${debugInfo.all_routes.join(', ')}`);
}
// Return the debug info for connection setup
return debugInfo;
} catch (error) {
logMessage(`Server connection test failed: ${error.message}`);
return null;
}
}
// Connect to WebSocket server
async function connectWebSocket() {
// First test connectivity to the server
logMessage('Testing server connectivity...');
const debugInfo = await testServerConnectivity();
// Use secure WebSocket (wss://) if the page is loaded over HTTPS
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// Get base path from URL
const basePath = window.location.pathname.replace(/\/+$/, '');
// Try both with and without base path for WebSocket connection
let serverUrl = `${protocol}//${window.location.hostname}${window.location.port ? ':' + window.location.port : ''}${basePath}/ws`;
logMessage(`Attempting to connect to WebSocket at ${serverUrl}...`);
// For compatibility, try the direct /ws path if the base path doesn't work
const fallbackUrl = `${protocol}//${window.location.hostname}${window.location.port ? ':' + window.location.port : ''}/ws`;
try {
socket = new WebSocket(serverUrl);
setupWebSocketHandlers();
// Set a timeout to try the fallback URL if the first one doesn't connect
setTimeout(() => {
if (socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CONNECTING) {
logMessage(`Connection to ${serverUrl} failed. Trying fallback URL: ${fallbackUrl}`);
socket = new WebSocket(fallbackUrl);
setupWebSocketHandlers();
}
}, 3000);
} catch (error) {
logMessage(`Error connecting to WebSocket: ${error.message}`);
resetUI();
}
}
// Set up WebSocket event handlers
function setupWebSocketHandlers() {
socket.onopen = () => {
logMessage('WebSocket connection established');
connectBtn.textContent = 'Disconnect';
startStreamBtn.disabled = false;
sceneSelect.disabled = false;
};
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
switch (message.action) {
case 'welcome':
userId = message.userId;
logMessage(`Connected with user ID: ${userId}`);
// Update scene options if server provides them
if (message.scenes && Array.isArray(message.scenes)) {
sceneSelect.innerHTML = '';
message.scenes.forEach(scene => {
const option = document.createElement('option');
option.value = scene;
option.textContent = scene.charAt(0).toUpperCase() + scene.slice(1);
sceneSelect.appendChild(option);
});
}
break;
case 'frame':
// Process incoming frame
processFrame(message);
break;
case 'start_stream':
if (message.success) {
isStreaming = true;
startStreamBtn.disabled = true;
stopStreamBtn.disabled = false;
logMessage(`Streaming started: ${message.message}`);
// Start FPS counter
startFpsCounter();
} else {
logMessage(`Error starting stream: ${message.error}`);
}
break;
case 'stop_stream':
if (message.success) {
isStreaming = false;
startStreamBtn.disabled = false;
stopStreamBtn.disabled = true;
logMessage('Streaming stopped');
// Stop FPS counter
stopFpsCounter();
} else {
logMessage(`Error stopping stream: ${message.error}`);
}
break;
case 'pong':
// Server responded to ping
break;
case 'change_scene':
if (message.success) {
logMessage(`Scene changed to ${message.scene}`);
} else {
logMessage(`Error changing scene: ${message.error}`);
}
break;
default:
logMessage(`Received message: ${JSON.stringify(message)}`);
}
};
socket.onclose = (event) => {
logMessage(`WebSocket connection closed (code: ${event.code}, reason: ${event.reason || 'none given'})`);
resetUI();
};
socket.onerror = (error) => {
logMessage(`WebSocket error. This is often caused by CORS issues or the server being inaccessible.`);
console.error('WebSocket error:', error);
resetUI();
};
}
// Disconnect from WebSocket server
function disconnectWebSocket() {
if (socket && socket.readyState === WebSocket.OPEN) {
// Stop streaming if active
if (isStreaming) {
sendStopStream();
}
// Close the socket
socket.close();
logMessage('Disconnected from server');
}
}
// Start streaming frames
function sendStartStream() {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
action: 'start_stream',
requestId: generateRequestId(),
fps: 16 // Default FPS
}));
}
}
// Stop streaming frames
function sendStopStream() {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
action: 'stop_stream',
requestId: generateRequestId()
}));
}
}
// Send keyboard input to server
function sendKeyboardInput(key, pressed) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
action: 'keyboard_input',
requestId: generateRequestId(),
key: key,
pressed: pressed
}));
}
}
// Send mouse input to server
function sendMouseInput(x, y) {
if (socket && socket.readyState === WebSocket.OPEN && isStreaming) {
socket.send(JSON.stringify({
action: 'mouse_input',
requestId: generateRequestId(),
x: x,
y: y
}));
}
}
// Change scene
function sendChangeScene(scene) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
action: 'change_scene',
requestId: generateRequestId(),
scene: scene
}));
}
}
// Process incoming frame
function processFrame(message) {
// Update FPS calculation
const now = performance.now();
if (lastFrameTime > 0) {
frameCount++;
}
lastFrameTime = now;
// Update the canvas with the new frame
if (message.frameData) {
gameCanvas.src = `data:image/jpeg;base64,${message.frameData}`;
}
}
// Generate a random request ID
function generateRequestId() {
return Math.random().toString(36).substring(2, 15);
}
// Log message to the connection info panel
function logMessage(message) {
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
const timestamp = new Date().toLocaleTimeString();
logEntry.textContent = `[${timestamp}] ${message}`;
connectionLog.appendChild(logEntry);
connectionLog.scrollTop = connectionLog.scrollHeight;
// Limit number of log entries
while (connectionLog.children.length > 100) {
connectionLog.removeChild(connectionLog.firstChild);
}
}
// Start FPS counter updates
function startFpsCounter() {
frameCount = 0;
lastFrameTime = 0;
// Update FPS display every second
fpsUpdateInterval = setInterval(() => {
fpsCounter.textContent = `FPS: ${frameCount}`;
frameCount = 0;
}, 1000);
}
// Stop FPS counter updates
function stopFpsCounter() {
if (fpsUpdateInterval) {
clearInterval(fpsUpdateInterval);
fpsUpdateInterval = null;
}
fpsCounter.textContent = 'FPS: 0';
}
// Reset UI to initial state
function resetUI() {
connectBtn.textContent = 'Connect';
startStreamBtn.disabled = true;
stopStreamBtn.disabled = true;
sceneSelect.disabled = true;
// Reset key indicators
for (const key in keyElements) {
keyElements[key].classList.remove('active');
}
// Stop FPS counter
stopFpsCounter();
// Reset streaming state
isStreaming = false;
}
// Event Listeners
connectBtn.addEventListener('click', () => {
if (socket && socket.readyState === WebSocket.OPEN) {
disconnectWebSocket();
} else {
connectWebSocket();
}
});
startStreamBtn.addEventListener('click', sendStartStream);
stopStreamBtn.addEventListener('click', sendStopStream);
sceneSelect.addEventListener('change', () => {
sendChangeScene(sceneSelect.value);
});
// Keyboard event listeners
document.addEventListener('keydown', (event) => {
const key = event.key.toLowerCase();
// Map key to action
let action = keyToAction[key];
if (!action && key === ' ') {
action = keyToAction[' ']; // Handle spacebar
}
if (action && !keyState[action]) {
keyState[action] = true;
// Update visual indicator
const keyElement = keyElements[key] ||
(key === ' ' ? keyElements['space'] : null) ||
(key === 'shift' ? keyElements['shift'] : null);
if (keyElement) {
keyElement.classList.add('active');
}
// Send to server
sendKeyboardInput(action, true);
}
// Prevent default actions for game controls
if (Object.keys(keyToAction).includes(key) || key === ' ') {
event.preventDefault();
}
});
document.addEventListener('keyup', (event) => {
const key = event.key.toLowerCase();
// Map key to action
let action = keyToAction[key];
if (!action && key === ' ') {
action = keyToAction[' ']; // Handle spacebar
}
if (action && keyState[action]) {
keyState[action] = false;
// Update visual indicator
const keyElement = keyElements[key] ||
(key === ' ' ? keyElements['space'] : null) ||
(key === 'shift' ? keyElements['shift'] : null);
if (keyElement) {
keyElement.classList.remove('active');
}
// Send to server
sendKeyboardInput(action, false);
}
});
// Mouse capture functions
function requestPointerLock() {
if (!mouseState.captured && pointerLockSupported) {
mouseTrackingArea.requestPointerLock = mouseTrackingArea.requestPointerLock ||
mouseTrackingArea.mozRequestPointerLock ||
mouseTrackingArea.webkitRequestPointerLock;
mouseTrackingArea.requestPointerLock();
logMessage('Mouse captured. Press ESC to release.');
}
}
function exitPointerLock() {
if (mouseState.captured) {
document.exitPointerLock = document.exitPointerLock ||
document.mozExitPointerLock ||
document.webkitExitPointerLock;
document.exitPointerLock();
logMessage('Mouse released.');
}
}
// Handle pointer lock change events
document.addEventListener('pointerlockchange', pointerLockChangeHandler);
document.addEventListener('mozpointerlockchange', pointerLockChangeHandler);
document.addEventListener('webkitpointerlockchange', pointerLockChangeHandler);
function pointerLockChangeHandler() {
if (document.pointerLockElement === mouseTrackingArea ||
document.mozPointerLockElement === mouseTrackingArea ||
document.webkitPointerLockElement === mouseTrackingArea) {
// Pointer is locked, enable mouse movement tracking
mouseState.captured = true;
document.addEventListener('mousemove', handleMouseMovement);
} else {
// Pointer is unlocked, disable mouse movement tracking
mouseState.captured = false;
document.removeEventListener('mousemove', handleMouseMovement);
// Reset mouse state
mouseState.x = 0;
mouseState.y = 0;
mousePosition.textContent = `Mouse: ${mouseState.x.toFixed(2)}, ${mouseState.y.toFixed(2)}`;
throttledSendMouseInput();
}
}
// Mouse tracking with pointer lock
function handleMouseMovement(event) {
if (mouseState.captured) {
// Use movement for mouse look when captured
const sensitivity = 0.005; // Adjust sensitivity
mouseState.x += event.movementX * sensitivity;
mouseState.y -= event.movementY * sensitivity; // Invert Y for intuitive camera control
// Clamp values
mouseState.x = Math.max(-1, Math.min(1, mouseState.x));
mouseState.y = Math.max(-1, Math.min(1, mouseState.y));
// Update display
mousePosition.textContent = `Mouse: ${mouseState.x.toFixed(2)}, ${mouseState.y.toFixed(2)}`;
// Send to server (throttled)
throttledSendMouseInput();
}
}
// Mouse click to capture
mouseTrackingArea.addEventListener('click', () => {
if (!mouseState.captured && isStreaming) {
requestPointerLock();
}
});
// Standard mouse tracking for when pointer is not locked
mouseTrackingArea.addEventListener('mousemove', (event) => {
if (!mouseState.captured) {
// Calculate normalized coordinates relative to the center of the tracking area
const rect = mouseTrackingArea.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;
// Calculate relative position from center (-1 to 1)
const relX = (event.clientX - rect.left - centerX) / centerX;
const relY = (event.clientY - rect.top - centerY) / centerY;
// Scale down for smoother movement
const scaleFactor = 0.05;
mouseState.x = relX * scaleFactor;
mouseState.y = -relY * scaleFactor; // Invert Y for intuitive camera control
// Update display
mousePosition.textContent = `Mouse: ${mouseState.x.toFixed(2)}, ${mouseState.y.toFixed(2)}`;
// Send to server (throttled)
throttledSendMouseInput();
}
});
// Throttle mouse movement to avoid flooding the server
const throttledSendMouseInput = (() => {
let lastSentTime = 0;
const interval = 50; // milliseconds
return () => {
const now = performance.now();
if (now - lastSentTime >= interval) {
sendMouseInput(mouseState.x, mouseState.y);
lastSentTime = now;
}
};
})();
// Toggle panel collapse/expand
function togglePanel(panelId) {
const panel = document.getElementById(panelId);
const button = panel.querySelector('.toggle-button');
if (panel.classList.contains('collapsed')) {
// Expand the panel
panel.classList.remove('collapsed');
button.textContent = '−'; // Minus sign
} else {
// Collapse the panel
panel.classList.add('collapsed');
button.textContent = '+'; // Plus sign
}
}
// Initialize the UI
resetUI();
// Make panel headers clickable
document.querySelectorAll('.panel-header').forEach(header => {
header.addEventListener('click', () => {
const panelId = header.parentElement.id;
togglePanel(panelId);
});
});
</script>
</body>
</html>