|
|
<!doctype html> |
|
|
<html> |
|
|
<head> |
|
|
<meta charset="utf-8" /> |
|
|
<title>Pong</title> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
|
|
|
|
|
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script> |
|
|
<style> |
|
|
html, body { margin:0; height:100%; background:#111; color:#eee; font-family: system-ui, sans-serif; } |
|
|
#overlay { |
|
|
position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; |
|
|
background: rgba(0,0,0,0.8); z-index: 9999; transition: opacity 200ms ease; |
|
|
} |
|
|
#overlay.hidden { opacity: 0; pointer-events: none; } |
|
|
.spinner { |
|
|
width: 64px; height: 64px; border: 6px solid #444; border-top-color: #09f; border-radius: 50%; |
|
|
animation: spin 0.9s linear infinite; |
|
|
} |
|
|
@keyframes spin { to { transform: rotate(360deg); } } |
|
|
#statusText { margin-top: 12px; color: #aaa; text-align: center; font-size: 14px; white-space: pre-line; } |
|
|
#app { padding: 16px; } |
|
|
button { padding: 8px 12px; background:#09f; color:#fff; border:none; border-radius:8px; cursor:pointer; } |
|
|
button:disabled { opacity: .5; cursor: not-allowed; } |
|
|
img#frame { image-rendering: pixelated; width: 240px; height: 240px; background:#222; display:block; margin-top:12px; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div id="overlay"> |
|
|
<div> |
|
|
<div class="spinner"></div> |
|
|
<div id="statusText">Loading model…</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="app"> |
|
|
<h1>Pong</h1> |
|
|
<div style="margin-bottom: 12px;"> |
|
|
<label style="display: block; margin-bottom: 8px;"> |
|
|
FPS: <input type="number" id="fpsInput" value="12" min="1" max="30" step="1" style="width: 60px; padding: 4px; margin-left: 8px;" /> |
|
|
<span style="color: #aaa; font-size: 12px; margin-left: 8px;">frames per second</span> |
|
|
</label> |
|
|
<label style="display: block; margin-bottom: 8px;"> |
|
|
Steps: <input type="number" id="stepsInput" value="4" min="1" max="10" step="1" style="width: 60px; padding: 4px; margin-left: 8px;" /> |
|
|
<span style="color: #aaa; font-size: 12px; margin-left: 8px;">diffusion steps</span> |
|
|
</label> |
|
|
</div> |
|
|
<div> |
|
|
<button id="startBtn" disabled>Start Stream</button> |
|
|
<button id="stopBtn" disabled>Stop Stream</button> |
|
|
</div> |
|
|
<img id="frame" alt="Latest frame" /> |
|
|
<div id="actionDisplay" style="margin-top: 12px; font-size: 16px; font-family: monospace;"> |
|
|
Action: <span id="actionValue">-</span> |
|
|
</div> |
|
|
<div id="fpsDisplay" style="margin-top: 8px; font-size: 16px; font-family: monospace;"> |
|
|
Achieved FPS: <span id="fpsValue">-</span> |
|
|
</div> |
|
|
<div id="waitingMessage" style="margin-top: 12px; padding: 8px; background: #333; border-radius: 4px; display: none; color: #ffa500;"> |
|
|
⏳ Another player is currently using the stream. Please wait for them to finish. |
|
|
</div> |
|
|
<div style="margin-top: 12px; padding: 8px; background: #222; border-radius: 4px; border-left: 3px solid #09f; color: #ccc; font-size: 13px;"> |
|
|
💡 <strong>Tip:</strong> Click anywhere on this page to enable keyboard controls. Use <strong>↑/↓ Arrow Keys</strong> or <strong>W/S</strong> to control the paddle. |
|
|
</div> |
|
|
<div style="margin-top: 12px; font-size: 14px; color: #aaa; line-height: 1.5;"> |
|
|
This demo uses a small transformer model trained with rectified flow matching to simulate Pong game frames conditioned on user inputs. The model generates 24×24 pixel frames in real-time using diffusion sampling with configurable steps. Performance targets ~16 FPS with 4 diffusion steps on GPU hardware. |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
const socket = io({ transports: ['websocket', 'polling'] }); |
|
|
|
|
|
const overlay = document.getElementById('overlay'); |
|
|
const statusText = document.getElementById('statusText'); |
|
|
const startBtn = document.getElementById('startBtn'); |
|
|
const stopBtn = document.getElementById('stopBtn'); |
|
|
const frameImg = document.getElementById('frame'); |
|
|
|
|
|
function setStatus(isReady) { |
|
|
if (!isReady) { |
|
|
|
|
|
overlay.classList.remove('hidden'); |
|
|
startBtn.disabled = true; |
|
|
stopBtn.disabled = true; |
|
|
statusText.textContent = 'Loading model…'; |
|
|
} else { |
|
|
|
|
|
overlay.classList.add('hidden'); |
|
|
startBtn.disabled = false; |
|
|
stopBtn.disabled = false; |
|
|
statusText.textContent = 'Ready'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
setStatus(false); |
|
|
|
|
|
socket.on('connect', () => { |
|
|
|
|
|
console.log('connected'); |
|
|
}); |
|
|
|
|
|
|
|
|
socket.on('server_status', (payload) => { |
|
|
const ready = !!(payload && payload.ready); |
|
|
console.log('Server status:', { ready }); |
|
|
setStatus(ready); |
|
|
}); |
|
|
|
|
|
|
|
|
startBtn.addEventListener('click', () => { |
|
|
const fps = parseInt(document.getElementById('fpsInput').value) || 16; |
|
|
const n_steps = parseInt(document.getElementById('stepsInput').value) || 1; |
|
|
socket.emit('start_stream', { n_steps: n_steps, cfg: 0.0, fps: fps, clamp: true }); |
|
|
}); |
|
|
stopBtn.addEventListener('click', () => { |
|
|
socket.emit('stop_stream'); |
|
|
}); |
|
|
|
|
|
const actionValue = document.getElementById('actionValue'); |
|
|
const fpsValue = document.getElementById('fpsValue'); |
|
|
const waitingMessage = document.getElementById('waitingMessage'); |
|
|
|
|
|
|
|
|
socket.on('stream_busy', (data) => { |
|
|
console.log('Stream is busy:', data); |
|
|
waitingMessage.style.display = 'block'; |
|
|
startBtn.disabled = true; |
|
|
}); |
|
|
|
|
|
socket.on('stream_available', (data) => { |
|
|
console.log('Stream is available:', data); |
|
|
waitingMessage.style.display = 'none'; |
|
|
startBtn.disabled = false; |
|
|
}); |
|
|
|
|
|
|
|
|
socket.on('frame', ({ frame, frame_index, action, fps }) => { |
|
|
frameImg.src = `data:image/png;base64,${frame}`; |
|
|
|
|
|
const actionLabels = ['START','NOOP', 'UP', 'DOWN']; |
|
|
actionValue.textContent = `${action} (${actionLabels[action] || 'UNKNOWN'})`; |
|
|
|
|
|
if (fps !== undefined) { |
|
|
fpsValue.textContent = fps.toFixed(1); |
|
|
} |
|
|
}); |
|
|
|
|
|
socket.on('error', (e) => { |
|
|
console.warn('server error', e); |
|
|
if (e && e.message) { |
|
|
console.error('Server error message:', e.message); |
|
|
|
|
|
if (e.message.includes('Another player') || e.message.includes('not the current player')) { |
|
|
waitingMessage.style.display = 'block'; |
|
|
waitingMessage.textContent = '⏳ ' + e.message; |
|
|
startBtn.disabled = true; |
|
|
} else { |
|
|
alert('Error: ' + e.message); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
let action = null; |
|
|
if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') { |
|
|
action = 2; |
|
|
} else if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') { |
|
|
action = 3; |
|
|
} |
|
|
if (action !== null) { |
|
|
socket.emit('action', { action }); |
|
|
e.preventDefault(); |
|
|
} |
|
|
}); |
|
|
|
|
|
document.addEventListener('keyup', (e) => { |
|
|
if (['ArrowUp', 'ArrowDown', 'w', 'W', 's', 'S'].includes(e.key)) { |
|
|
socket.emit('action', { action: 1 }); |
|
|
e.preventDefault(); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|