Uni-app 红黑扑克模拟游戏复盘:跨端边界、计时器与牌型 Bug
本文目录
这是一份基于公开仓库当前源码的静态工程复盘,检查页面结构、牌组算法、局间计时、余额状态和跨端 API。它不是运行截图,也没有在微信开发者工具或真机上完成构建验收;本文区分代码证据与建议。
项目结构与边界
主页面位于 pages/index/index.vue,入口 main.js 创建 Vue 根实例,pages.json 只注册一个页面。页面把牌组生成、发牌、牌面解析、牌型计分和结果展示拆成多个方法,可沿 getPorker → deal → points → comparePoints → mesPrint 追踪一局游戏。
当前玩法在浏览器侧生成两方手牌,并把名为余额的数值存进本机存储。本次审阅的页面源码没有支付或服务端资金请求,因此应理解为本地虚拟筹码模拟,而不是钱包或可提现资产;这不构成对其他版本或已发布应用的判断。客户端可改写本地存储,任何余额都不能被服务器当作可信账本。
仓库树包含微信小程序与 App Plus 的开发构建输出、打包文件和 source map,但主页面使用浏览器 DOM API。源码与多个平台的产物并存,不代表源码实际具备跨端兼容性;生成物也不能代替锁文件和可复现构建说明。
本轮静态清点的目录树共 53 个文件,其中 unpackage/dist 下有 39 个开发构建文件(含 4 个 source map)。这是一次公开默认分支快照的计数,不代表后续提交不变,也不构成构建通过的证据。
截至本次核验,GitHub 仓库元数据没有声明许可证,根目录也没有 LICENSE 文件。本文因此只保留原创静态审阅和必要的短代码示例,不把仓库快照、题材素材或构建产物整份镜像到博客;如需再分发,应先确认权利来源。
需要优先修复的问题
1. 第一轮倒计时结束时会读到尚未初始化的变量
倒计时回调先调用 clearInterval(waittimer),随后才在同一代码块里用 let waittimer = setInterval(...) 声明变量。块级 let 在初始化前处于暂时性死区;第一次倒计时走到该语句时会抛出 ReferenceError,后续揭示阶段和下一局无法按预期启动。这是静态控制流问题,不必等随机牌局才能触发。
把轮次状态和计时器句柄放到页面状态中,使用单一阶段机调度下注、开牌、结果和下一局,并在页面卸载时停止所有定时器。不要在 interval 回调里层层创建新 interval,却只保留最外层句柄。
倒计时还有一个边界偏差:显示值从 20 开始,但代码递减后要小于 0 才结算,因而结算时机与屏幕显示的秒数不一致。应在状态机中明确何时从倒计时进入揭牌阶段,并用边界测试覆盖 1、0 和页面离开场景。
2. 同花与同花顺判断使用了错误的链式比较
牌型分支的表达式相当于 a == b == c。JavaScript 会先算出 a == b 的布尔值,再拿这个布尔值和字符串花色比较;它不是数学上的三值相等,因此真实的同花也不会进入对应分支。同花顺分支又依赖同一个判断,结果会连带失效。
应把牌解析为结构化对象 { rank, suit },再用 cards.every(card => card.suit === cards[0].suit) 表示同花。计分不要把类别和点数硬塞进巨大乘数:使用牌型等级与关键牌序列的元组逐项比较,明确 A-2-3、平局和花色优先级。当前用 indexOf 返回最大值玩家,平分时会固定偏向先出现的红方,也需要明确规则。
对子分支还有一个关键牌遗漏:它固定读取排序后的第二、第三张来组成对子分数,但对子可能出现在前两张,也可能出现在后两张。于是 K、2、2 与 Q、2、2 都会把 2 当作对子点数和踢脚牌,算成同分;较大的单张 K 没有参与比较。应先识别实际重复的 rank,再按“对子点数、踢脚牌”两级比较。
3. 用随机比较器排序不是可靠的洗牌
源码中的 randomsort 方法通过 sort(() => Math.random() > 0.5 ? -1 : 1) 打乱牌组。排序比较器应满足一致的反对称、传递等性质;每次比较都抛硬币会违反这个契约,结果可能依赖引擎的排序过程,不能视为均匀洗牌。若玩法只是本地娱乐,可用 Fisher-Yates;一旦涉及竞赛、奖品或任何有价值的筹码,随机源和发牌结果必须由可信服务端设计,不能把客户端 Math.random() 当成公平性或安全保证。
function shuffle(source, random = Math.random) {
const cards = source.slice();
for (let i = cards.length - 1; i > 0; i -= 1) {
const j = Math.floor(random() * (i + 1));
[cards[i], cards[j]] = [cards[j], cards[i]];
}
return cards;
}
const sameSuit = (cards) =>
cards.every((card) => card.suit === cards[0].suit);
4. 页面是 H5 DOM 实现,不是可直接运行的多端页面
模板使用 div/span,脚本用 document.querySelector、window.localStorage、innerHTML、alert 和 DOM 的 onclick。这些写法把视图状态绕开 Vue,也依赖 H5 环境;出现小程序/App Plus 编译目录并不能让这些 API 自动存在于原生页面运行时。直接改 DOM 与内联样式会绕过响应式状态,测试也很难隔离。
若目标是 H5,就把产品范围写清楚、只构建 H5,并用 Vue 数据和事件绑定替代手工 DOM。若目标包含微信小程序或 App,则用 view/text 等跨端组件、页面数据驱动渲染、@tap 事件和 uni 平台 API;本地余额读写应迁移到 uni.getStorageSync / uni.setStorageSync,并校验异常、空值和数值范围。uni-app 官方文档区分页面生命周期,也明确需要在 onUnload 清理监听;不要把 H5 mounted 当作所有平台唯一的页面生命周期。
5. 余额和下注状态容易被用户端篡改或进入无效状态
本地存储适合保存娱乐进度,不是权限或资金系统。当前代码直接把页面余额写入 localStorage,投注池和胜负统计也主要存在 DOM 文本里。刷新、改存储、重复点击或切换平台后,状态可能不一致;数据也没有明确的金额类型、下注边界、错误恢复和持久化版本。
初始化还把本地存储读出的余额写进 innerHTML。既然用户可以修改存储值,就不能把它当作可信 HTML;应验证为有限、非负数值,再通过文本插值显示。当前结算也把获胜侧自己的奖池乘以二后返还,却没有把另一侧投入纳入计算;项目没有服务端账本或清晰的资金守恒规则,因此这只能是本地虚拟积分演示,不能表达真实资产或真实派奖。
对纯离线小游戏,至少把余额、两侧下注、手牌、阶段和历史记录收进响应式状态;所有下注经过纯函数校验,检查有限数值、正数、余额和阶段,并用一次状态更新扣减余额。加载旧存档时做 schema/version 迁移。产品文案要明确虚拟积分仅保存在本机、无充值与提现,不要把浏览器本地数值包装成真实资金。
6. 固定宽度和屏幕坐标不适合手机操作
棋桌、下注区等多处使用固定的 30rem 宽度,余额和历史区使用固定偏移。手机窄屏、系统字体缩放、横竖屏变化都可能让控件溢出或互相遮挡;页面背景直接拉伸到容器宽高也会变形。改用弹性网格、容器最大宽度、相对间距和最小触控尺寸,为安全区与软键盘留空间,并验证 320、375、390 CSS px 及横屏布局。
7. 项目缺少可复现的维护入口
当前公开仓库的 README 极短,仓库快照没有项目级 package.json、锁文件、自动化测试或 CI 工作流;仅凭提交的开发产物无法确认 HBuilderX、Vue/uni-app 与构建插件版本。source map 和多平台开发输出也会增加仓库噪声。建议先写清目标平台、IDE/CLI 版本和干净环境构建命令,再决定是否将生成目录纳入版本控制;发布产物与源代码分离,并检查 source map 是否包含不应公开的路径或内容。
8. Android Manifest 声明了与牌局页面不相称的权限
当前 manifest.json 的 Android 权限数组包含 CAMERA、RECORD_AUDIO、READ_CONTACTS、WRITE_CONTACTS、READ_PHONE_STATE、CALL_PHONE、ACCESS_COARSE_LOCATION 和 ACCESS_FINE_LOCATION 等声明;同一清单的 app-plus.modules 为空。当前默认分支的业务源码只有一个牌局页面,使用本地 DOM 与存储,没有找到相机、录音、联系人、电话或定位功能的调用。这是清单与现有源码之间的权限面不匹配信号,应逐项确认后删除不需要的权限。
静态源码不能证明这些权限一定会触发系统弹窗,也不能代替最终安装包检查:HBuilderX、插件或构建工具可能合并其他声明,Android 版本和 target SDK 也会改变授权时机。发布前应检查最终 APK/AAB 的合并 Manifest,只保留实际功能所需的最小权限;在干净构建中核对安装后权限清单,并用 Android 真机验证首次启动和对应功能的授权时机。本轮未构建 APK/AAB、未安装应用,也未观察系统权限弹窗,因此不把该风险描述成已复现的运行时行为。
推荐重构顺序
- 用纯函数拆出 createDeck、shuffle、deal、evaluateHand、compareHands 和 settleRound;先补同花、A-2-3、平局及低对子踢脚牌测试,再接页面。
- 用有限状态机管理下注、发牌、结果展示和新一局;用单一计时器管理器,并定义页面隐藏、卸载和重新进入时的暂停/恢复策略。
- 把 DOM 查询和直接 innerHTML 全部移出业务方法,改为 Vue 响应式状态、模板插值和声明式事件。
- 选择一个真实目标平台先打通干净构建,再逐平台适配存储、生命周期、触摸和导航 API;不要把 unpackage/dist/dev 目录当作兼容性证明。
- 补 README、锁文件、代码格式检查和 CI:依赖安装、静态检查、算法测试、目标平台构建都能在新环境复现。
- 做概率分布模拟、A-2-3 与平分用例、倒计时边界、连续点击、余额损坏、页面切后台/卸载、弱设备和窄屏实测;逐项审查最终 Android Manifest 的权限;若将来加入有价值的筹码,先暂停上线,重新设计服务端账本、随机性、风控与合规边界。
审阅结论
这个小项目的亮点是把发牌、比较、计分和反馈拆成可读的方法,也已经有一局对战的完整产品雏形;现在最值得留下的是玩法原型,而不是当前 DOM 与计分实现。优先修复计时器、同花判断、洗牌和跨端运行时边界,再决定要不要迁移 UI。由于本次拿到的是公开 GitHub 源码快照,没有在本机检出并安装 HBuilderX/目标平台依赖,本文不声称构建通过、小游戏运行正常或已经完成真机验收。
项目源仓库:haoge568/-
参考:uni-app 页面与生命周期、uni-app 本地存储 API、MDN Array.prototype.sort()、MDN Math.random()。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。