hanli/web/server.php
hant cb4b955bca Implement Server-Sent Events (SSE) for real-time battle streaming
Redesign web battle system from buffered to streaming architecture:

Backend Changes:
- New SSEOutput class for real-time event streaming to clients
- GameSession::streamBattle() for SSE-based battle execution
- Enhanced Screen::delay() to support SSE timing and buffering modes
- New /api/game/battle-stream endpoint handling SSE connections

Frontend Changes:
- Enhanced sendInput() to detect battle command (input "1")
- New streamBattle() function using EventSource for SSE connections
- Real-time log display matching terminal experience
- Event handlers for start, message, complete, error events

Benefits:
✓ Real-time streaming instead of waiting for complete battle
✓ Web frontend experience identical to terminal
✓ Lightweight implementation without WebSocket
✓ Automatic browser reconnection support
✓ ANSI colors fully preserved
✓ Backward compatible for non-battle screens

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-07 10:50:30 +08:00

247 lines
6.1 KiB
PHP

<?php
/**
* Web 游戏服务器入口
* 使用方法: php -S 0.0.0.0:8080 web/server.php
*/
// 设置错误报告
error_reporting(E_ALL);
ini_set('display_errors', 0);
// 自动加载
require_once __DIR__ . '/../vendor/autoload.php';
use Game\Core\UserManager;
use Game\Core\GameSession;
use Game\Core\WebInput;
// 设置 Web 模式
WebInput::getInstance()->setWebMode(true);
// 启动会话
session_start();
// 设置响应头
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// 处理 OPTIONS 预检请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// 获取请求路径
$requestUri = $_SERVER['REQUEST_URI'];
$path = parse_url($requestUri, PHP_URL_PATH);
// 静态文件处理
if ($path === '/' || $path === '/index.html') {
header('Content-Type: text/html; charset=utf-8');
readfile(__DIR__ . '/index.html');
exit;
}
// API 路由
$response = ['success' => false, 'message' => '未知请求'];
try {
switch ($path) {
case '/api/register':
$response = handleRegister();
break;
case '/api/login':
$response = handleLogin();
break;
case '/api/logout':
$response = handleLogout();
break;
case '/api/status':
$response = handleStatus();
break;
case '/api/game/render':
$response = handleGameRender();
break;
case '/api/game/input':
$response = handleGameInput();
break;
case '/api/game/battle-stream':
// SSE 实时战斗流
handleBattleStream();
exit;
break;
default:
// 检查是否是静态文件
$filePath = __DIR__ . $path;
if (file_exists($filePath) && is_file($filePath)) {
$ext = pathinfo($path, PATHINFO_EXTENSION);
$mimeTypes = [
'js' => 'application/javascript',
'css' => 'text/css',
'html' => 'text/html',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'gif' => 'image/gif',
];
header('Content-Type: ' . ($mimeTypes[$ext] ?? 'application/octet-stream'));
readfile($filePath);
exit;
}
http_response_code(404);
$response = ['success' => false, 'message' => '未找到'];
}
} catch (Exception $e) {
http_response_code(500);
$response = ['success' => false, 'message' => '服务器错误: ' . $e->getMessage()];
}
echo json_encode($response, JSON_UNESCAPED_UNICODE);
// ============ 处理函数 ============
function getInput(): array
{
$input = file_get_contents('php://input');
return json_decode($input, true) ?? [];
}
function handleRegister(): array
{
$data = getInput();
$username = $data['username'] ?? '';
$password = $data['password'] ?? '';
$userManager = new UserManager();
$result = $userManager->register($username, $password);
if ($result['success']) {
$_SESSION['user_id'] = $result['userId'];
$_SESSION['username'] = $username;
}
return $result;
}
function handleLogin(): array
{
$data = getInput();
$username = $data['username'] ?? '';
$password = $data['password'] ?? '';
$userManager = new UserManager();
$result = $userManager->login($username, $password);
if ($result['success']) {
$_SESSION['user_id'] = $result['userId'];
$_SESSION['username'] = $username;
}
return $result;
}
function handleLogout(): array
{
session_destroy();
return ['success' => true, 'message' => '已退出登录'];
}
function handleStatus(): array
{
if (empty($_SESSION['user_id'])) {
return ['success' => false, 'loggedIn' => false, 'message' => '未登录'];
}
return [
'success' => true,
'loggedIn' => true,
'userId' => $_SESSION['user_id'],
'username' => $_SESSION['username'] ?? '未知',
];
}
function handleGameRender(): array
{
if (empty($_SESSION['user_id'])) {
return ['success' => false, 'message' => '请先登录'];
}
$session = new GameSession($_SESSION['user_id']);
$output = $session->render();
return [
'success' => true,
'output' => $output,
];
}
function handleGameInput(): array
{
if (empty($_SESSION['user_id'])) {
return ['success' => false, 'message' => '请先登录'];
}
$data = getInput();
$input = $data['input'] ?? '';
$session = new GameSession($_SESSION['user_id']);
$output = $session->handleInput($input);
// 如果输出是数组(时间戳数据),直接返回
if (is_array($output)) {
return array_merge(['success' => true], $output);
}
// 否则返回纯文本输出
return [
'success' => true,
'output' => $output,
];
}
/**
* 处理 SSE 实时战斗流
*/
function handleBattleStream(): void
{
if (empty($_SESSION['user_id'])) {
http_response_code(401);
echo "data: " . json_encode(['error' => '请先登录']) . "\n\n";
return;
}
// 从URL参数或POST数据获取输入
$input = $_GET['input'] ?? $_POST['input'] ?? '';
// 设置 SSE 响应头
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no'); // 禁用代理缓冲
// 发送初始化消息
echo "event: start\n";
echo "data: " . json_encode(['message' => '战斗开始']) . "\n\n";
ob_flush();
flush();
// 创建游戏会话并流式处理战斗
try {
$session = new GameSession($_SESSION['user_id']);
$session->streamBattle($input);
} catch (Exception $e) {
echo "event: error\n";
echo "data: " . json_encode(['message' => $e->getMessage()]) . "\n\n";
ob_flush();
flush();
}
}