卡牌游戏对局调试工具
一、背景
随着游戏上线,每天产出大量玩家操作数据。当玩家来反馈bug时,除了其提供的录屏,这些操作数据也是很重要的参考资料。然而:
- 线上出 Bug 时需要查数据仓库看玩家状态,流程繁琐
- 从数据仓库导出的 CSV 是原始日志,可读性差
因此我开发了一个工具,把数据渲染成易读的样子,另外顺便加一些调试性操作进去,比如加一张卡什么的。
为了给 windows 系统的同事一起用,正好我又喜欢 rust,于是选择了用 Tauri 进行开发。
本应用是一个 Tauri 桌面应用,为卡牌游戏的开发和测试提供对局调试能力,包括查询实时游戏状态、回放对局记录等功能。
二、整体架构
三、核心功能实现
一、SSO 登录认证模块
内网的 API 需要在请求头中带 SSO Cookie 才能访问。Cookie 会过期,需要在 401 时自动用 TOTP 重新登录。
二、游戏状态查询
数据来源:
游戏的状态数据(血量、护甲、手牌、弃牌区、牌库)存放在 KV 存储中。
UI 展示:
查询结果包含两个玩家的完整状态:HP、护甲、手牌(横向展示)、弃牌区(竖向滚动,红底)、牌库(竖向滚动,蓝底)。卡牌 ID 通过映射表转为中文名。
<div class="players flex gap-6">
<div v-for="player in gameInfo.players" class="player flex-1">
<h3>{{ player.player_id }}</h3>
<p>生命值: {{ player.hp }}</p>
<!-- 手牌横向排列 -->
<div class="hand flex flex-wrap gap-1">
<span v-for="card in player.cards_hand" class="card bg-gray-200">
{{ get_card_name(card.card_id) }}
</span>
</div>
<!-- 弃牌区竖向滚动 -->
<div class="discard max-h-48 overflow-y-auto">
<div v-for="card in player.cards_discard" class="card bg-red-200">
{{ get_card_name(card.card_id) }}
</div>
</div>
<!-- 牌库竖向滚动 -->
<div class="deck max-h-48 overflow-y-auto">
<div v-for="card in player.cards_deck" class="card bg-blue-200">
{{ get_card_name(card.card_id) }}
</div>
</div>
</div>
</div>三、对局记录回放
对局记录有两个数据来源,两个页面都是通过 CSV 上传 + papaparse 解析。
Recorder(数仓版本):
数据源是数据仓库表,包含出牌前的状态快照。CSV 中每一行是一次出牌操作,包含 player_id、card_id、before_player_hp、before_player_buff 等字段。
解析后按 turn_count 分组,逐回合展示:
const onFileChange = (e) => {
Papa.parse(file, {
header: true,
complete: (result) => {
// 解析 before_player_buff JSON 字符串
const raw = result.data.map((row) => ({
...row,
before_player_buff: JSON.parse(row.before_player_buff || "[]"),
play_time: Number(row.play_time),
}));
raw.sort((a, b) => a.play_time - b.play_time);
records.value = raw;
},
});
};Recorder2(日志版本):
数据源是服务端广播日志,CSV 中 msg 字段包含完整的协议日志。需要从字符串中用正则提取关键字段,再二次解析内层 JSON:
const playerId = msg.match(/playerId:(\d+)/)?.[1];
const action = msg.match(/action:(\d+)/)?.[1];
const timestamp = msg.match(/"timestamp":(\d+)/)?.[1];
const dataStr = msg.match(/data:(\{.*\})$/)?.[1];
const dataObj = JSON.parse(dataStr);
const inner = JSON.parse(dataObj.data); // 二次解析
这里的难点是日志字段是格式化的字符串而非 JSON,需要先通过正则提取外层字段,再逐层解析嵌套结构。action 通过映射表转为可读名称:
const action_map = {
1000: "出牌",
1030: "选择卡牌",
1040: "结束回合",
1050: "投降",
2028: "爆牌",
2037: "受到伤害",
};四、Settings 窗口的特殊处理
需求:
设置窗口(OA 账号密码、Dashboard Token)是一个独立的配置界面,关闭时不应该真正退出,“X” 按钮应该只是隐藏窗口。
实现方案:
WebviewWindowBuilder::new(app, "preference", WebviewUrl::App("/#/settings"))
.visible(false) // 初始不显示
.build()?;
// 劫持关闭事件,隐藏而非销毁
if let Some(win) = app_handle.get_webview_window(&"preference") {
win.on_window_event(move |event| {
if let WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close(); // 阻止默认关闭
win.hide().unwrap(); // 隐藏窗口
}
});
}
// 菜单触发时显示
app.on_menu_event(move |app_handle, event| {
if event.id().0.as_str() == "preferences" {
let win = app_handle.get_webview_window("preference");
win.show().unwrap();
win.set_focus().unwrap();
}
});api.prevent_close() 是 Tauri 提供的阻止关闭机制,配合 win.hide() 实现"假关闭"效果。相比每次重新创建窗口,隐藏/显示的方式可以保留输入框中的未保存内容。
五、技术选型
| 层 | 技术 | 选择理由 |
|---|---|---|
| 桌面框架 | Tauri 2 | 比 Electron 体积小(< 10MB),Rust 后端性能好 |
| 前端框架 | Vue 3 + Vite | 轻量,Composition API 适合复杂交互 |
| 路由 | vue-router | Hash 模式,兼容 Tauri 的 file:// 协议 |
| CSV 解析 | papaparse | 成熟稳定,支持流式解析大文件 |
| HTTP 客户端 | reqwest (Rust) | Tauri 原生 Rust,异步支持好 |
| TOTP | totp-rs | Rust 生态中最完善的 TOTP 实现 |
| 样式 | 纯 CSS | 桌面工具类应用,不需要组件库 |
六、总结
本应用是一个典型的"把重复工作工具化"的项目。它将查 KV 系统、解析日志、管理 SSO 登录等这些分散的操作整合到一个桌面 App 中。
优势:
- SSO Cookie 自动管理 — 缓存 + 401 自动重登,省去手动获取 Cookie 的步骤
- 双源日志回放 — 数仓版本展示精确的出牌前状态,日志版本展示实时广播消息,对照使用定位问题更高效