Files

312 lines
12 KiB
HTML
Raw Permalink 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; }
.row { display: flex; flex-wrap: wrap; gap: 12px; }
.row > .panel { flex: 1 1 260px; }
button { margin-right: 6px; padding: 4px 10px; cursor: pointer; }
input { width: 60px; }
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.snake2 { background: #ff9800; }
.cell.snake3 { background: #9c27b0; }
.cell.head0 { background: #2e7d32; }
.cell.head1 { background: #1565c0; }
.cell.head2 { background: #e65100; }
.cell.head3 { background: #6a1b9a; }
.cell.food { background: #f44336; border-radius: 50%; }
.cell.stone1 { color: #333; }
.cell.stone2 { color: #888; }
#log { width: 100%; height: 240px; font-family: Consolas, monospace; font-size: 12px; }
.hint { color: #888; font-size: 12px; }
</style>
</head>
<body>
<h1>WebSocket 联调测试台</h1>
<p class="hint">
通用流程:选玩法(贪吃蛇再选人数)→ 各玩家「取 token」→「连接」→「匹配」→「准备」。<br>
<b>五子棋</b>:点棋盘空格落子。<b>贪吃蛇</b>:玩家1 方向键 ↑→↓←,玩家2 W/D/S/A,玩家3 I/L/K/J,玩家4 T/G/H/F(先点一下本页获取键盘焦点)。
</p>
<div class="panel">
<h2>玩法与人数</h2>
玩法
<select id="game" onchange="onGameChange()">
<option value="gobang">五子棋(15×15,2 人,回合制)</option>
<option value="snake">贪吃蛇(21×21,实时 200ms/步)</option>
</select>
<span id="seatsWrap" style="display:none">
 人数
<select id="seats">
<option value="2">2 人场</option>
<option value="3">3 人场</option>
<option value="4">4 人场</option>
</select>
</span>
<span class="status" id="turn">等待开局</span>
</div>
<div class="row">
<div class="panel"><h2>玩家 1</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>
<div class="status" id="stA">未连接</div>
</div>
<div class="panel"><h2>玩家 2</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>
<div class="status" id="stB">未连接</div>
</div>
<div class="panel" id="panelC" style="display:none"><h2>玩家 3</h2>
用户ID <input id="uidC" value="3">
<button onclick="getToken('C')">取 token</button>
<button onclick="connect('C')">连接</button>
<button onclick="match('C')">匹配</button>
<button onclick="ready('C')">准备</button>
<button onclick="leave('C')">离开</button>
<div class="status" id="stC">未连接</div>
</div>
<div class="panel" id="panelD" style="display:none"><h2>玩家 4</h2>
用户ID <input id="uidD" value="4">
<button onclick="getToken('D')">取 token</button>
<button onclick="connect('D')">连接</button>
<button onclick="match('D')">匹配</button>
<button onclick="ready('D')">准备</button>
<button onclick="leave('D')">离开</button>
<div class="status" id="stD">未连接</div>
</div>
</div>
<div class="panel">
<h2>棋盘</h2>
<div class="grid" id="board"></div>
<div id="scores" class="hint"></div>
</div>
<div class="panel">
<h2>消息日志(贪吃蛇的 tick 很密集,默认不记录)</h2>
<label class="hint"><input type="checkbox" id="logTick"> 记录 tick 事件</label>
<textarea id="log" readonly></textarea>
</div>
<script>
const KEYS = ['A', 'B', 'C', 'D'];
const S = {};
KEYS.forEach(k => { S[k] = { uid: 0, token: '', ws: null, seq: 1, room: '', size: 0, players: [], turn: 0, board: [] }; });
let snakeData = null;
function gameId() { return document.getElementById('game').value; }
function seats() { return parseInt(document.getElementById('seats').value, 10); }
function log(msg) {
const el = document.getElementById('log');
el.value += new Date().toLocaleTimeString() + ' ' + msg + '\n';
el.scrollTop = el.scrollHeight;
}
function onGameChange() {
const isSnake = gameId() === 'snake';
document.getElementById('seatsWrap').style.display = isSnake ? 'inline' : 'none';
const n = isSnake ? seats() : 2;
document.getElementById('panelC').style.display = n >= 3 ? '' : 'none';
document.getElementById('panelD').style.display = n >= 4 ? '' : 'none';
resetBoard();
}
document.getElementById('seats').addEventListener('change', onGameChange);
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) {
const p = gameId() === 'snake' ? { game_id: gameId(), seats: seats() } : { game_id: gameId() };
send(k, 'hall.match', p);
}
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 === 'hall.matched') {
log(k + ' 匹配成功:' + 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 (isTick && 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() {
KEYS.forEach(k => { S[k].board = []; });
snakeData = null;
document.getElementById('scores').textContent = '';
render();
}
function render() {
const grid = document.getElementById('board');
grid.innerHTML = '';
if (gameId() === 'snake') { renderSnake(); return; }
const ref = KEYS.map(k => S[k]).find(s => s.board.length) || S.A;
const board = ref.board || [];
if (!board.length) { document.getElementById('turn').textContent = '等待开局'; return; }
const n = ref.size || Math.round(Math.sqrt(board.length));
const cell = 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' + (v === 1 ? ' stone1' : v === 2 ? ' stone2' : '');
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);
}
document.getElementById('scores').textContent = snakeData.snakes
.map(sn => 'uid=' + sn.user_id + ' 分' + sn.score + (sn.alive ? '' : '(阵亡)')).join(' | ');
document.getElementById('turn').textContent = '贪吃蛇进行中:1=方向键 2=WASD 3=IJKL 4=TFGH';
}
function place(index) {
if (gameId() === 'snake') return;
const ref = KEYS.map(k => S[k]).find(s => s.board.length) || S.A;
const n = ref.size || Math.round(Math.sqrt(ref.board.length));
const turnUid = (ref.players || [])[ref.turn || 0];
const who = KEYS.find(k => S[k].uid === turnUid);
if (!who) { log('现在不是已连接玩家的回合,或尚未开局'); return; }
send(who, 'room.action', { action: 'place', payload: { row: Math.floor(index / n), col: index % n } });
}
// 四个玩家各自的键位
const KEYMAP = {
A: { ArrowUp: 'up', ArrowRight: 'right', ArrowDown: 'down', ArrowLeft: 'left' },
B: { w: 'up', d: 'right', s: 'down', a: 'left' },
C: { i: 'up', l: 'right', k: 'down', j: 'left' },
D: { t: 'up', g: 'right', h: 'down', f: 'left' },
};
document.addEventListener('keydown', (e) => {
if (gameId() !== 'snake') return;
const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
for (const k of KEYS) {
if (KEYMAP[k][key]) {
send(k, 'room.action', { action: 'turn', payload: { dir: KEYMAP[k][key] } });
e.preventDefault();
return;
}
}
});
onGameChange();
</script>
</body>
</html>