Files
zt-online-game-backend/web/ws-test.html
T

258 lines
9.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>WebSocket 联调测试台</title>
<style>
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; margin: 20px; color: #222; }
h1 { font-size: 20px; }
.panel { border: 1px solid #ddd; border-radius: 6px; padding: 12px; margin-bottom: 12px; }
.panel h2 { font-size: 15px; margin: 0 0 8px; }
button { margin-right: 6px; padding: 4px 10px; cursor: pointer; }
input { width: 70px; }
select { padding: 3px; }
.status { font-weight: bold; margin-left: 8px; }
.grid { display: grid; gap: 1px; margin: 8px 0; width: max-content; background: #eee; padding: 1px; }
.cell { background: #fff; text-align: center; cursor: pointer; user-select: none; }
.cell.snake0 { background: #4caf50; }
.cell.snake1 { background: #2196f3; }
.cell.head0 { background: #2e7d32; }
.cell.head1 { background: #1565c0; }
.cell.food { background: #ff5722; border-radius: 50%; }
#log { width: 100%; height: 260px; font-family: Consolas, monospace; font-size: 12px; }
.hint { color: #888; font-size: 12px; }
</style>
</head>
<body>
<h1>WebSocket 联调测试台</h1>
<p class="hint">
通用流程:① 选玩法 → ② A、B 各「取 token」→ ③ 各「连接」→ ④ 各「匹配」→ ⑤ 各「准备」。<br>
五子棋 / 井字棋:在棋盘上点空格落子。贪吃蛇:<b>玩家 A 用方向键 ↑→↓←,玩家 B 用 W / D / S / A</b>(点一下本页获得键盘焦点)。
</p>
<div class="panel">
<h2>玩法</h2>
选择
<select id="game" onchange="resetBoard()">
<option value="gobang">五子棋(15×15,连五胜)</option>
<option value="snake">双人贪吃蛇(21×21,实时推进)</option>
<option value="tictactoe">井字棋(3×3,示例玩法)</option>
</select>
<span class="status" id="turn">等待开局</span>
</div>
<div class="panel">
<h2>玩家 A</h2>
用户ID <input id="uidA" value="1">
<button onclick="getToken('A')">取 token</button>
<button onclick="connect('A')">连接</button>
<button onclick="match('A')">匹配</button>
<button onclick="ready('A')">准备</button>
<button onclick="leave('A')">离开</button>
<span class="status" id="stA">未连接</span>
</div>
<div class="panel">
<h2>玩家 B</h2>
用户ID <input id="uidB" value="2">
<button onclick="getToken('B')">取 token</button>
<button onclick="connect('B')">连接</button>
<button onclick="match('B')">匹配</button>
<button onclick="ready('B')">准备</button>
<button onclick="leave('B')">离开</button>
<span class="status" id="stB">未连接</span>
</div>
<div class="panel">
<h2>棋盘</h2>
<div class="grid" id="board"></div>
<div id="scores" class="hint"></div>
</div>
<div class="panel">
<h2>消息日志(贪吃蛇的 tick 事件很密集,默认只记非 tick 消息)</h2>
<label class="hint"><input type="checkbox" id="logTick"> 记录 tick 事件</label>
<textarea id="log" readonly></textarea>
</div>
<script>
const S = {
A: { uid: 1, token: '', ws: null, seq: 1, room: '', size: 0, players: [], turn: 0, board: [] },
B: { uid: 2, token: '', ws: null, seq: 1, room: '', size: 0, players: [], turn: 0, board: [] },
};
let snakeData = null; // 贪吃蛇最新画面数据
function gameId() { return document.getElementById('game').value; }
function log(msg) {
const el = document.getElementById('log');
el.value += new Date().toLocaleTimeString() + ' ' + msg + '\n';
el.scrollTop = el.scrollHeight;
}
async function getToken(k) {
const uid = document.getElementById('uid' + k).value;
const res = await fetch('/api/v1/dev/token?user_id=' + uid);
const json = await res.json();
if (json.code !== 0) { log(k + ' 取 token 失败: ' + JSON.stringify(json)); return; }
S[k].uid = parseInt(uid, 10);
S[k].token = json.data.access_token;
log(k + ' 已获取 token(uid=' + S[k].uid + ')');
}
function connect(k) {
if (!S[k].token) { log(k + ' 请先取 token'); return; }
const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(scheme + '://' + location.host + '/ws?token=' + S[k].token);
S[k].ws = ws;
ws.onopen = () => { setStatus(k, '已连接'); log(k + ' ws 已连接'); };
ws.onclose = (e) => { setStatus(k, '已断开(' + e.code + ')'); log(k + ' ws 断开: ' + e.code); };
ws.onerror = () => log(k + ' ws 出错(见服务端日志)');
ws.onmessage = (e) => { handle(k, JSON.parse(e.data)); };
}
function setStatus(k, text) { document.getElementById('st' + k).textContent = text; }
function send(k, type, payload) {
if (!S[k].ws || S[k].ws.readyState !== 1) { log(k + ' 未连接'); return; }
S[k].ws.send(JSON.stringify({ type: type, seq: S[k].seq++, payload: payload }));
}
function match(k) { send(k, 'hall.match', { game_id: gameId() }); }
function ready(k) { send(k, 'room.ready', { ready: true }); }
function leave(k) { send(k, 'room.leave', {}); }
function handle(k, env) {
const isTick = env.type === 'room.event' && env.payload && env.payload.event === 'tick';
if (!isTick || document.getElementById('logTick').checked) {
log(k + ' ← ' + env.type + ' ' + (env.payload ? JSON.stringify(env.payload) : ''));
}
if (env.type === 'room.state') {
const p = env.payload || {};
S[k].room = p.room_id || S[k].room;
const st = p.state || {};
if (gameId() === 'snake' && st.snakes) {
snakeData = { size: st.size, snakes: st.snakes, foods: st.foods || [] };
renderSnake();
return;
}
if (st.board) {
S[k].size = st.size || Math.round(Math.sqrt(st.board.length));
S[k].players = st.players || S[k].players;
S[k].turn = st.turn || 0;
S[k].board = st.board;
render();
}
}
if (env.type === 'room.event' && env.payload && env.payload.event === 'tick' && gameId() === 'snake') {
const d = env.payload.payload || {};
snakeData = { size: d.size, snakes: d.snakes, foods: d.foods || [] };
renderSnake();
}
if (env.type === 'room.finished') {
const w = (env.payload || {}).winner;
document.getElementById('turn').textContent = w ? '本局结束,获胜者 uid=' + w : '本局结束:平局';
}
if (env.type === 'room.closed') { log(k + ' 房间已关闭: ' + (env.payload || {}).reason); }
}
function resetBoard() {
S.A.board = []; S.B.board = []; snakeData = null;
document.getElementById('scores').textContent = '';
render();
}
function render() {
const grid = document.getElementById('board');
grid.innerHTML = '';
if (gameId() === 'snake') { renderSnake(); return; }
const ref = S.A.board.length ? S.A : S.B;
const board = ref.board || [];
if (!board.length) { document.getElementById('turn').textContent = '等待开局'; return; }
const n = ref.size || Math.round(Math.sqrt(board.length));
const cell = n === 3 ? 60 : 26;
grid.style.gridTemplateColumns = 'repeat(' + n + ', ' + cell + 'px)';
for (let i = 0; i < board.length; i++) {
const el = document.createElement('div');
const v = board[i] || 0;
el.className = 'cell';
el.style.width = cell + 'px';
el.style.height = cell + 'px';
el.style.lineHeight = cell + 'px';
el.textContent = v === 1 ? '●' : (v === 2 ? '○' : '');
el.onclick = () => place(i);
grid.appendChild(el);
}
const turnUid = (ref.players || [])[ref.turn || 0];
document.getElementById('turn').textContent = turnUid ? '轮到 uid=' + turnUid : '等待开局';
}
function renderSnake() {
const grid = document.getElementById('board');
grid.innerHTML = '';
if (!snakeData) { document.getElementById('turn').textContent = '等待开局'; return; }
const n = snakeData.size;
const cell = 20;
grid.style.gridTemplateColumns = 'repeat(' + n + ', ' + cell + 'px)';
const owner = {}; // 坐标 -> 蛇序号
snakeData.snakes.forEach((sn, si) => {
sn.body.forEach((b, bi) => { owner[b] = { si: si, head: bi === 0 }; });
});
const foods = {};
(snakeData.foods || []).forEach(f => { foods[f] = true; });
for (let i = 0; i < n * n; i++) {
const el = document.createElement('div');
el.style.width = cell + 'px';
el.style.height = cell + 'px';
let cls = 'cell';
if (owner[i]) { cls += owner[i].head ? ' head' + owner[i].si : ' snake' + owner[i].si; }
else if (foods[i]) { cls += ' food'; }
el.className = cls;
grid.appendChild(el);
}
const parts = snakeData.snakes.map(sn => 'uid=' + sn.user_id + ' 分' + sn.score + (sn.alive ? '' : '(已阵亡)'));
document.getElementById('scores').textContent = parts.join(' | ');
document.getElementById('turn').textContent = '贪吃蛇进行中:A 用方向键,B 用 WASD';
}
function place(index) {
if (gameId() === 'snake') return;
const ref = S.A.board.length ? S.A : S.B;
const n = ref.size || Math.round(Math.sqrt(ref.board.length));
const row = Math.floor(index / n);
const col = index % n;
const turnUid = (ref.players || [])[ref.turn || 0];
let who = null;
if (turnUid && S.A.uid === turnUid) who = 'A';
else if (turnUid && S.B.uid === turnUid) who = 'B';
if (!who) { log('现在不是 A/B 的回合,或尚未开局'); return; }
const payload = n === 3 ? { index: index } : { row: row, col: col };
send(who, 'room.action', { action: 'place', payload: payload });
}
function turn(k, dir) {
if (gameId() !== 'snake') return;
send(k, 'room.action', { action: 'turn', payload: { dir: dir } });
}
document.addEventListener('keydown', (e) => {
const arrow = { ArrowUp: 'up', ArrowRight: 'right', ArrowDown: 'down', ArrowLeft: 'left' };
const wasd = { w: 'up', d: 'right', s: 'down', a: 'left' };
if (arrow[e.key]) { turn('A', arrow[e.key]); e.preventDefault(); return; }
const k = (e.key || '').toLowerCase();
if (wasd[k]) { turn('B', wasd[k]); e.preventDefault(); }
});
</script>
</body>
</html>