Skip to content

【WIP】feat: UNO Flip 模式 - #211

Open
DoyoDia wants to merge 2 commits into
mainfrom
feat/uno-flip-mode
Open

【WIP】feat: UNO Flip 模式#211
DoyoDia wants to merge 2 commits into
mainfrom
feat/uno-flip-mode

Conversation

@DoyoDia

@DoyoDia DoyoDia commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

以独立游戏模式(gameMode: 'classic' | 'flip')实现 UNO Flip。规则基线核对自 Mattel 官方说明书 GDR44,设计文档见 docs/uno-flip-mode-design.md

为什么不是村规

村规插件的能力边界是 GameState → GameState,只能改变动作处理流程;而 UNO Flip 改变的是数据模型本身——双面牌、8 种颜色、6 种新卡型、对手可见手牌背面、独立计分表。

官方规则里三个容易搞错的点

  • 两面都没有 0 牌(1–9 各两张,18 张/色)→ 村规「0 牌交换手牌」在 Flip 下必须禁用
  • 亮面没有带色 +2:带色罚摸牌是 +1,+2 是万能牌,两个不同卡型
  • Flip 卡整体反转弃牌堆:新顶牌是本轮第一张弃牌,不是刚打出的 Flip 卡

质疑罚则也不同:Flip 是 2/4,经典是 4/6;wild 在 Flip 计 40 分而非 50。

核心架构决策

双面牌采用「当前活动面 + back」而非 { id, light, dark }

  • 现有 187 处 card.type / card.color 读取点零改动canPlayCard / 计分 / 机器人估值自动作用于活动面
  • classic 模式 back === undefined,零成本,既有行为逐位不变
  • 官方「按结束时所在面计分」天然满足
  • 代价(deckHash 稳定性、翻面一致性)用双面归一化身份 + 单一 flipAll 入口 + 不变量测试解决

牌组配对固定(来自实物牌组记录,已交叉校验与官方配比逐项吻合)。这不是细节:配对固定意味着可以从对手手牌背面反推其正面持牌,是本模式的核心博弈层。

可见性

PlayerView 新增 handBacks:对手手牌背面对所有人可见,本人手牌一律剥离 back——官方规则下你看不到自己手牌的背面。批量广播路径也做了剔除。有专门的测试守着这条。

机器人

新卡型估值 + 翻面时机判断;从对手背面反推持牌按难度分级开放(novice/easy 不用、normal 只看颜色、hard 完整反推)。

公平性守卫测试:构造两个只有「机器人自己手牌背面」不同的局面,断言所有评分因子完全一致。这条守卫用「故意注入作弊代码 → 测试变红 → 移除 → 变绿」验证过有效。

村规

11 条 Flip 专属村规。经典与 Flip 的叠加/挡罚共用一套谓词stack-rules.ts),两个既有插件按模式自动分派,经典语义有回归测试锁定。预设按模式分派,切换模式时归一化村规(服务端权威校验)。

顺带修复的既有缺陷(与 Flip 无关)

新增的随机对局性质测试(3 机器人自动打完整局,断言每步动作都能被引擎接受)压出两个死锁,经典模式对照组同样复现,影响已发布的「派对」「疯狂」预设

  1. 「同数字全出」连出打光同数字牌后补 PASS → 轮次已推进,PASS 非法 → 机器人循环 break 且不重新调度 → 永久卡死
  2. 「同数字全出」+「7 牌交换」:出 7 立刻进入选交换目标阶段,后续连出动作全部非法 → 同样卡死

另外修了:isFunctionCard 只认经典卡型导致「空手赢不算」对 Flip 罚摸牌失效;GameSession.create 不传 settings 时首牌可能是万能牌导致测试约 4% 偶发失败。

验证

shared 399 / server 129 / mcp 4 全部通过
构建:shared / server / client / admin / mcp 全绿
e2e:flip-demo 全程开着「疯狂」预设打完整局,28 条断言通过
     经典模式 smoke 无回归

packages/e2e/flip-demo.mjspnpm --filter @uno-online/e2e flip-demo)真开一局并断言可见性、双面模型、翻面、牌框、设置面板布局等。

不含发版

按仓库惯例发版是独立 PR,本 PR 不含版本号提升与 CHANGELOG。

已知未完成

  • 移动端玩家列表未展示对手背面(空间受限,桌面端与罗盘已覆盖)
  • Flip 卡面资源包只预留了索引段(100–153 / 200–253),尚无内置主题资源,Flip 卡型走内置矢量渲染

🤖 Generated with Claude Code

doyo_dia added 2 commits July 30, 2026 00:29
以独立游戏模式(gameMode: 'classic' | 'flip')而非村规开关的形式设计
UNO Flip,规则基线核对自 Mattel 官方说明书(GDR44)。

主要内容:
- 官方规则基线:112 张双面牌组成、卡牌效果、质疑罚则、计分表
- 双面卡数据模型:采用「活动面 + back」方案,现有 187 处卡牌读取点零改动
- 翻面事务 flipAll:弃牌堆整体反转、条件式罚摸(Wild Draw Color)
- 可见性协议:handBacks 下发对手手牌背面,自己手牌背面必须裁剪
- 前端:村规下方模式开关、左右牌堆亮暗面视觉、对手背面渲染
- 34 条现有村规兼容矩阵 + 11 条 Flip 专属村规
- 7 阶段实施计划与风险清单
以独立游戏模式(gameMode: 'classic' | 'flip')实现 UNO Flip,规则基线核对自
Mattel 官方说明书(GDR44)。设计文档见 docs/uno-flip-mode-design.md。

数据模型
- Color 扩展为 8 色(亮面红蓝绿黄 / 暗面粉青橙紫),CardType 新增 6 种卡型
- 双面牌采用「当前活动面 + back」而非「id + 两个面」:现有 187 处卡牌读取点
  零改动,canPlayCard / 计分 / 机器人估值自动作用于活动面,classic 零成本
- 按实物牌组的固定配对表生成 112 张牌(来源见 constants/flip-deck.ts 注释)
- cardToIdentity 输出与活动面无关的双面身份,避免翻面触发反作弊误报

规则引擎
- flipAll:弃牌堆整体反转 + 两个牌堆 + 所有手牌,唯一翻面入口
- 新卡效果:+1 / +5 / 跳过全体 / Flip / 万能+2 / 摸到指定色(条件式罚摸)
- Flip 质疑罚则 2/4(经典是 4/6)、独立计分表、首牌为 Flip 时立即翻面
- CHOOSE_COLOR 增加所属牌面校验,防止把当前色设成场上不存在的颜色

可见性
- PlayerView 新增 handBacks:对手手牌背面对所有人可见,本人手牌剥离 back
- 批量广播路径同样剔除本人的 handBacks

机器人
- 新卡型估值 + 翻面时机判断;困难难度可从对手背面反推持牌(难度分级)
- 守卫测试:只有自己背面不同的两个局面,评分因子必须完全一致

村规
- 11 条 Flip 专属村规(叠加/挡罚/万能出/保留颜色/背面透视/摸色上限/暗面翻倍)
- 经典与 Flip 的叠加、挡罚共用一套谓词(stack-rules.ts),按模式自动分派
- 预设按模式分派;切换模式时归一化村规,服务端做权威校验

前端
- 房间设置新增「游戏模式」分段控件,Flip 下隐藏不适用的经典村规
- 对手手牌位逐张渲染背面、暗面牌框(黑灰边 + 灰白描边)、暗面主题
- 装饰牌左亮右暗、翻面一次性转场、Flip 卡面资源包索引段、教程与规则面板

顺带修复(既有问题,与 Flip 无关)
- 「同数字全出」连出打光同数字牌后补 PASS 导致机器人死锁
- 「同数字全出」+「7 牌交换」连出 7 时后续动作非法导致死锁
- isFunctionCard 只认经典卡型,「空手赢不算」对 Flip 罚摸牌失效
- GameSession.create 不传 settings 时首牌可能是万能牌,导致测试偶发失败
@DoyoDia DoyoDia changed the title feat: UNO Flip 模式 【WIP】feat: UNO Flip 模式 Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant