# Lucky Dice 核心流程规格 ## 文档信息 - 来源需求:[lucky-dice-core-flow-requirements.md](../Requirements/lucky-dice-core-flow-requirements.md) - 适用范围:FishDice 第一版普通骰子主循环与 Lucky Dice 特殊分支 - 文档目标:把玩法需求拆成可实现、可测试、可扩展的系统规格 ## 设计目标 Lucky Dice 核心流程需要支持两层玩法: 1. 普通骰子主循环:玩家按骰子集合配置 Roll 普通骰子,根据组合事实匹配规则并执行奖励或特殊触发行为。 2. Lucky Dice 特殊流程:当普通骰子出现双四叶草或配置声明的触发条件时,进入独立的特殊结果池,经过筛选、权重随机和模式解析后启动目标玩法。 第一版只做核心闭环,不实现复杂动画、远端热更、完整短玩法内容和复杂运营权重策略。系统边界必须保留扩展点,避免后续新增组合或特殊结果时不断堆硬编码分支。 第一版默认值: - 普通骰子数量默认 2 颗,但必须来自 DiceSet 配置。 - Lucky Dice 结果槽数量默认 3 格,但必须来自 Lucky Dice 结果或目标玩法配置。 - 规则系统、组合事实和展示流程不得把 2 颗骰子或 3 格结果槽写死为不可扩展假设。 ## 总体架构 核心模块按职责拆分为以下几层: ```text Roll 输入层 → 骰子结果生成层 → 组合事实提取层 → 组合规则匹配层 → 行为执行层 → Lucky Dice 流程层 → 目标玩法模式解析层 → 目标玩法启动层 ``` 各层只依赖前一层产出的结构化数据,不直接读取 UI 状态或动画状态。 ### 模块职责 | 模块 | 职责 | 不负责 | | --- | --- | --- | | DiceSetConfigProvider | 提供骰子集合、骰子数量、结果槽数量等配置 | 执行 Roll 或业务行为 | | DiceRollService | 按骰子集合配置生成普通骰子或 Lucky Dice 结果 | 决定奖励、玩法入口 | | ComboFactBuilder | 把普通 Roll 结果转换为结构化组合事实 | 执行业务行为 | | ComboRuleMatcher | 按优先级匹配组合规则 | 直接发奖或进玩法 | | RollActionDispatcher | 按行为类型分发到通用执行器 | 按组合 key 维护巨大方法 map | | LuckyDiceFlowService | 构建候选池、筛选、随机、生成可展示结果槽 | 直接决定目标模式细节 | | TargetModeResolver | 把 ResultKey/TargetKey 解析为 ModeKey | 执行目标玩法内容 | | TargetModeLauncher | 统一启动目标玩法 | 参与 Lucky Dice 随机 | | RollTraceLogger | 记录一次 Roll 的关键决策链路 | 影响玩法决策 | ## 核心流程 ### 普通 Roll 流程 ```text PlayerRollRequest → DiceSetConfigProvider.GetNormalDiceSet(request) → DiceRollService.Roll(NormalDiceSet) → ComboFactBuilder.Build(result) → ComboRuleMatcher.Match(facts, ruleSet) → RollActionDispatcher.Execute(matchedRule.Actions) → grant_reward:发放普通奖励并结束 → grant_reward + add_multiplier:发奖并应用 Clover bonus → trigger_lucky_dice:进入 LuckyDiceFlowService ``` 普通 Roll 第一版默认两颗骰子,骰子面为: ```text 2 / 3 / 4 / 5 / 6 / Clover ``` 骰子数量必须从 `DiceSetConfig.DiceCount` 读取。第一版配置值可以是 2,但 `DiceRollService`、`ComboFactBuilder` 和 matcher 都必须按 `Faces.Count` 或 `DiceCount` 处理,不读取固定的左骰、右骰两个位置。 ### Lucky Dice 流程 ```text TriggerLuckyDiceAction → LuckyDiceFlowService.BuildCandidatePool(context) → LuckyDiceFlowService.ApplyFilters(pool, context) → LuckyDiceFlowService.ResolveEmptyPoolFallback(filteredPool, context) → LuckyDiceFlowService.WeightedPick(filteredPool, context) → LuckyDiceFlowService.BuildResultSlots(result, slotConfig) → LuckyDicePresentation.ShowResultSlots(resultSlots) → TargetModeResolver.Resolve(result, context) → TargetModeLauncher.Launch(targetEntry) → TargetModeCompletion 回到主循环 ``` 第一版 Lucky Dice 默认展示 3 格相同特殊图标,不要求实现托盘、飞入、粒子爆发、白烟转场等表现。结果槽数量必须从 Lucky Dice 结果或目标玩法配置读取,后续可扩展为 2 格、4 格或按玩法定义不同槽数。 ## 数据模型 ### DiceSetConfig 骰子集合配置定义一次 Roll 使用哪套骰子、投几颗骰子,以及该集合是否拥有展示槽概念。 ```csharp public sealed class DiceSetConfig { public string DiceSetId { get; init; } public DiceSetType SetType { get; init; } public int DiceCount { get; init; } public IReadOnlyList Faces { get; init; } public int? DefaultResultSlotCount { get; init; } } ``` 约束: - `DiceCount` 必须大于 0。 - 第一版普通骰子 `DiceCount = 2`,但核心逻辑不得依赖该常量。 - Lucky Dice 如果不真实 Roll 多颗特殊骰,可以将 `DiceCount` 理解为结果选择次数;第一版只随机一次,再由展示槽配置生成多个相同槽。 - `DefaultResultSlotCount` 用于展示层默认槽数,第一版 Lucky Dice 为 3。 ### DiceFace 普通骰子面与 Lucky Dice 特殊结果必须区分集合类型,不能混用同一个裸字符串列表。 ```csharp public enum DiceSetType { Normal, Lucky } public readonly struct DiceFace { public DiceSetType SetType { get; } public string Key { get; } public int? NumberValue { get; } } ``` 约束: - 普通数字面使用 `Key = "2" ... "6"`,`NumberValue` 为对应数字。 - 普通四叶草使用 `Key = "clover"`,`NumberValue = null`。 - Lucky Dice 特殊结果使用 `DiceSetType.Lucky`,例如 `rocket`、`thief`。 - 业务代码不得到处裸写这些 key,必须集中在定义表或常量入口。 ### DiceRollResult ```csharp public sealed class DiceRollResult { public string RollSessionId { get; init; } public string DiceSetId { get; init; } public DiceSetType SetType { get; init; } public int DiceCount { get; init; } public IReadOnlyList Faces { get; init; } public int Multiplier { get; init; } public string TriggerSource { get; init; } } ``` 约束: - `DiceCount` 必须等于本次配置要求的投掷数量。 - `Faces.Count` 必须等于本次实际产出的骰子面数量。 - 普通 Roll 中 `DiceCount` 和 `Faces.Count` 不一致时,应拒绝进入规则匹配并记录错误。 ### ComboFacts `ComboFacts` 是普通组合匹配的唯一输入。 ```csharp public sealed class ComboFacts { public DiceSetType DiceSetType { get; init; } public int DiceCount { get; init; } public IReadOnlyList Faces { get; init; } public IReadOnlyDictionary FaceCounts { get; init; } public int NumberSum { get; init; } public IReadOnlyList NumberValues { get; init; } public int CloverCount { get; init; } public bool HasClover { get; init; } public string NormalizedKey { get; init; } public string OrderedKey { get; init; } } ``` 归一化规则: - 默认按无序组合处理。 - `2 + clover` 与 `clover + 2` 都归一为 `2_clover`。 - `clover + clover` 归一为 `clover_clover`。 - N 颗骰子的 `NormalizedKey` 由所有面 key 排序后拼接,例如 `2_2_clover`、`3_3_3`。 - 后续若需要有序语义,由规则显式声明使用 `OrderedKey`。 约束: - `ComboFacts` 不能假设只有 2 颗骰子。 - matcher 必须基于 `DiceCount`、`Faces`、`FaceCounts`、`NumberValues` 等统计信息判断。 - 只有有序规则才能读取 `OrderedKey`,普通规则默认使用无序事实。 ### ComboRule ```csharp public sealed class ComboRule { public string RuleId { get; init; } public int Priority { get; init; } public ComboMatcherSpec Matcher { get; init; } public IReadOnlyList Actions { get; init; } public bool StopAfterMatched { get; init; } = true; } ``` 第一版规则: | Priority | Matcher | Actions | | --- | --- | --- | | 100 | ExactCombo `clover_clover` | `trigger_lucky_dice` | | 50 | ContainsFace `clover` count 1 | `grant_reward`, `add_multiplier` | | 10 | AllNumbers | `grant_reward` | 匹配策略: - 按 `Priority` 从高到低匹配。 - 同优先级按配置顺序匹配。 - 第一版默认命中第一条 `StopAfterMatched = true` 的规则后停止。 - 未命中任何规则时走可观测兜底,默认发普通奖励或返回明确错误,由产品配置决定。 ### ComboMatcherSpec ```csharp public sealed class ComboMatcherSpec { public string MatcherType { get; init; } public string ComboKey { get; init; } public string FaceKey { get; init; } public int? RequiredCount { get; init; } public int? MinCount { get; init; } public int? MaxCount { get; init; } public int? DiceCount { get; init; } public int? OfAKindCount { get; init; } public int? MinSum { get; init; } public int? MaxSum { get; init; } public bool UseOrderedKey { get; init; } } ``` 首批 matcher: - `ExactCombo` - `AllNumbers` - `ContainsFace` - `FaceCount` - `FaceCountRange` - `DiceCount` - `NumberPair` - `NumberOfAKind` - `NumberSumRange` 新增组合语义优先新增规则配置;只有出现新的通用匹配能力时才新增 matcher。 N 颗骰子扩展示例: | Matcher | 例子 | 含义 | | --- | --- | --- | | `FaceCountRange` | `faceKey = clover`, `minCount = 2` | 至少 2 个 Clover | | `DiceCount` | `diceCount = 3` | 只匹配 3 骰规则 | | `NumberOfAKind` | `ofAKindCount = 3` | 任意数字三连 | | `NumberSumRange` | `minSum = 18`, `maxSum = 24` | 点数和落在 18 到 24 | ### RollActionSpec ```csharp public sealed class RollActionSpec { public string ActionType { get; init; } public IReadOnlyDictionary Params { get; init; } } ``` 首批 action: - `grant_reward` - `add_multiplier` - `trigger_lucky_dice` - `enter_mode` - `show_popup` 执行层只按 `ActionType` 找执行器,不按组合 key 找方法。 ### LuckyDiceCandidate ```csharp public sealed class LuckyDiceCandidate { public string ResultKey { get; init; } public string TargetKey { get; init; } public int? ResultSlotCount { get; init; } public int Weight { get; init; } public bool Enabled { get; init; } public int MinLevel { get; init; } public IReadOnlySet SourceFilter { get; init; } public CooldownRule CooldownRule { get; init; } public bool IsDefault { get; init; } } ``` 第一版候选配置: | ResultKey | TargetKey | ResultSlotCount | Weight | Enabled | Mode 默认结果 | | --- | --- | --- | --- | --- | --- | | `rocket` | `slap_down` | 3 | 1 | true | `slap_down_normal` | | `thief` | `treasure_heist` | 3 | 1 | true | `treasure_heist_normal` | `ResultSlotCount` 允许为空。为空时使用 Lucky Dice 集合配置或目标玩法配置中的默认槽数。 ### LuckyDiceResultSlots Lucky Dice 随机只选中一个特殊结果;展示层按槽配置生成实际槽位。 ```csharp public sealed class LuckyDiceResultSlots { public string ResultKey { get; init; } public string TargetKey { get; init; } public int SlotCount { get; init; } public IReadOnlyList SlotResultKeys { get; init; } } ``` 第一版要求: - `SlotCount` 默认 3。 - `SlotResultKeys` 第一版全部填入同一个 `ResultKey`。 - 后续如某玩法需要不同槽位结果,可通过配置扩展 `SlotResultKeys` 的生成策略。 ### LuckyDiceContext ```csharp public sealed class LuckyDiceContext { public string RollSessionId { get; init; } public string TriggerSource { get; init; } public int NormalDiceCount { get; init; } public int LuckyResultSlotCount { get; init; } public int PlayerLevel { get; init; } public int Multiplier { get; init; } public bool IsTutorial { get; init; } public IReadOnlyDictionary RuntimeTags { get; init; } } ``` ### TargetModeEntry ```csharp public sealed class TargetModeEntry { public string TargetKey { get; init; } public string ModeKey { get; init; } public string ResultKey { get; init; } public int Multiplier { get; init; } public string TriggerSource { get; init; } public string RollSessionId { get; init; } } ``` ## 接口边界 ### 组合事实构建 ```csharp public interface IComboFactBuilder { ComboFacts Build(DiceRollResult result); } ``` 要求: - 只接受普通骰子结果构建普通组合事实。 - 如果传入 Lucky Dice 结果,应返回错误或拒绝处理,避免两套骰子语义混用。 - 输出必须保留 DiceCount、原始 Faces、FaceCounts、NumberValues、NormalizedKey、OrderedKey。 - 不允许只读取固定两个骰子位置;必须遍历 `Faces`。 ### 组合规则匹配 ```csharp public interface IComboRuleMatcher { ComboRuleMatchResult Match(ComboFacts facts, IReadOnlyList rules); } ``` 要求: - matcher 由 `MatcherType` 注册,不由组合 key 注册。 - 匹配结果需要包含命中的 `RuleId`、`Priority`、`MatcherType` 和关键输入 facts,供日志追踪。 ### 行为执行 ```csharp public interface IRollActionExecutor { string ActionType { get; } RollActionResult Execute(RollActionContext context, RollActionSpec action); } ``` 要求: - `RollActionDispatcher` 只维护 `ActionType -> IRollActionExecutor` 注册表。 - 不允许维护 `clover_clover -> SomeMethod` 这类随组合数量增长的 map。 - 多个 action 按规则配置顺序执行。 - action 失败时需要返回结构化失败结果,并进入统一日志。 ### Lucky Dice 筛选 ```csharp public interface ILuckyDiceCandidateFilter { string FilterType { get; } IReadOnlyList Apply( IReadOnlyList candidates, LuckyDiceContext context); } ``` 首批 filter: - `EnabledFilter` - `ProgressFilter` - `SourceFilter` - `CooldownFilter` - `TutorialFilter` 筛选器按固定顺序执行,顺序建议为: ```text Enabled → Progress → Source → Cooldown → Tutorial ``` ### 权重随机 ```csharp public interface IWeightedPicker { LuckyDiceCandidate Pick( IReadOnlyList candidates, LuckyDiceContext context, IRandomSource random); } ``` 要求: - 权重小于等于 0 的候选不参与普通随机。 - 随机源可注入,便于测试和回放。 - 选中结果必须记录候选池快照、权重、随机值和最终 ResultKey。 ### Lucky Dice 结果槽构建 ```csharp public interface ILuckyDiceResultSlotBuilder { LuckyDiceResultSlots BuildSlots( LuckyDiceCandidate selected, LuckyDiceContext context, DiceSetConfig luckyDiceSetConfig); } ``` 要求: - 槽数优先级:候选结果配置 `ResultSlotCount` → 目标玩法配置 → Lucky Dice 集合配置 `DefaultResultSlotCount`。 - 第一版每个槽都展示同一个 `ResultKey`。 - 槽数小于等于 0 时返回结构化错误并进入 Lucky Dice 兜底。 ### 模式解析 ```csharp public interface ITargetModeResolver { TargetModeEntry Resolve(LuckyDiceCandidate selected, LuckyDiceContext context); } ``` 解析优先级: 1. `TutorialModeRule` 2. `BonusModeRule` 3. `DefaultModeRule` 第一版映射: | ResultKey | TargetKey | Default ModeKey | | --- | --- | --- | | `rocket` | `slap_down` | `slap_down_normal` | | `thief` | `treasure_heist` | `treasure_heist_normal` | ## 兜底与错误处理 ### 普通组合未命中 处理策略: 1. 记录 `RollSessionId`、原始 Faces、NormalizedKey。 2. 使用配置的普通奖励兜底,或返回可见错误给调试面板。 3. 不进入 Lucky Dice。 第一版建议默认发普通奖励兜底,避免玩家流程中断。 ### Lucky Dice 候选池为空 处理策略: 1. 尝试使用配置的 default result。 2. 如果 default result 不可用,执行普通奖励兜底。 3. 记录空池原因:初始候选数量、每个 filter 过滤前后数量、最终兜底类型。 ### 目标模式解析失败 处理策略: 1. 记录 `ResultKey`、`TargetKey`、上下文标签。 2. 如果存在 TargetKey 默认模式,降级到默认模式。 3. 如果不存在默认模式,返回 Lucky Dice 失败并执行普通奖励兜底。 ## 可观测性 每次 Roll 必须拥有同一个 `RollSessionId`,贯穿普通 Roll、规则匹配、Lucky Dice、模式解析和目标玩法启动。 最小日志字段: ```text RollSessionId TriggerSource NormalFaces NormalDiceCount ComboNormalizedKey MatchedRuleId MatchedRulePriority ExecutedActions LuckyDiceInitialCandidates LuckyDiceFilteredCandidates LuckyDiceFallbackType LuckyDiceSelectedResultKey LuckyDiceSelectedTargetKey LuckyDiceResultSlotCount LuckyDiceSlotResultKeys ResolvedModeKey FinalOutcome ``` 日志目标: - 复盘一次 Roll 为什么发奖、为什么进 Lucky Dice、为什么进某个玩法。 - 发现配置问题时能看出是规则未命中、候选池为空、权重异常还是模式解析失败。 ## 配置示例 第一版可以使用本地静态配置或 ScriptableObject;远端配置热更新不在范围内。 ### 骰子集合配置示例 ```json { "diceSetId": "normal_main", "setType": "Normal", "diceCount": 2, "faces": ["2", "3", "4", "5", "6", "clover"] } ``` ```json { "diceSetId": "lucky_dice", "setType": "Lucky", "diceCount": 1, "defaultResultSlotCount": 3, "faces": ["rocket", "thief", "chest", "bomb", "key"] } ``` ### 普通组合规则示例 ```json [ { "ruleId": "normal_clover_clover_lucky_dice", "priority": 100, "matcher": { "matcherType": "ExactCombo", "comboKey": "clover_clover" }, "actions": [ { "actionType": "trigger_lucky_dice", "params": {} } ], "stopAfterMatched": true }, { "ruleId": "normal_number_clover_bonus", "priority": 50, "matcher": { "matcherType": "ContainsFace", "faceKey": "clover", "requiredCount": 1 }, "actions": [ { "actionType": "grant_reward", "params": {} }, { "actionType": "add_multiplier", "params": { "reason": "clover_bonus" } } ], "stopAfterMatched": true }, { "ruleId": "normal_all_numbers_reward", "priority": 10, "matcher": { "matcherType": "AllNumbers" }, "actions": [ { "actionType": "grant_reward", "params": {} } ], "stopAfterMatched": true } ] ``` 后续多骰规则示例: ```json [ { "ruleId": "normal_at_least_two_clovers_lucky_dice", "priority": 100, "matcher": { "matcherType": "FaceCountRange", "faceKey": "clover", "minCount": 2 }, "actions": [ { "actionType": "trigger_lucky_dice", "params": {} } ], "stopAfterMatched": true }, { "ruleId": "normal_three_of_a_kind_bonus", "priority": 60, "matcher": { "matcherType": "NumberOfAKind", "ofAKindCount": 3 }, "actions": [ { "actionType": "grant_reward", "params": {} }, { "actionType": "add_multiplier", "params": { "reason": "three_of_a_kind" } } ], "stopAfterMatched": true } ] ``` ### Lucky Dice 候选示例 ```json [ { "resultKey": "rocket", "targetKey": "slap_down", "resultSlotCount": 3, "weight": 1, "enabled": true, "minLevel": 0, "sourceFilter": ["normal_roll"], "isDefault": true }, { "resultKey": "thief", "targetKey": "treasure_heist", "resultSlotCount": 3, "weight": 1, "enabled": true, "minLevel": 0, "sourceFilter": ["normal_roll"], "isDefault": false } ] ``` ## 测试规格 ### 组合事实测试 - `2 + 3` 生成 `NumberSum = 5`、`CloverCount = 0`、`NormalizedKey = "2_3"`。 - `2 + clover` 与 `clover + 2` 都生成 `NormalizedKey = "2_clover"`。 - `clover + clover` 生成 `CloverCount = 2`、`NormalizedKey = "clover_clover"`。 - `2 + 2 + clover` 生成 `DiceCount = 3`、`NumberValues = [2, 2]`、`NormalizedKey = "2_2_clover"`。 - Lucky Dice 特殊结果不能被普通 `ComboFactBuilder` 当成普通组合处理。 ### 规则匹配测试 - `clover_clover` 命中优先级 100 的 Lucky Dice 规则。 - `2_clover` 命中优先级 50 的 Clover bonus 规则。 - `2_6` 命中优先级 10 的 AllNumbers 规则。 - 当多条规则同时可命中时,高优先级先执行。 - 3 骰配置下,`FaceCountRange clover min 2` 能命中任意至少 2 个 Clover 的组合。 - `NumberOfAKind count 3` 能命中三个相同数字,不依赖具体数字 key。 ### 行为执行测试 - `grant_reward` 调用奖励执行器。 - `trigger_lucky_dice` 调用 Lucky Dice 流程入口。 - dispatcher 只按 `ActionType` 分发,不依赖组合 key。 - 未注册的 action 返回结构化错误并记录日志。 ### Lucky Dice 测试 - Enabled=false 的候选被过滤。 - 玩家等级不足时候选被 `ProgressFilter` 过滤。 - 候选池为空时使用 default result。 - default result 不可用时走普通奖励兜底并记录日志。 - 权重随机可以通过固定随机源得到可预测结果。 - Lucky Dice 结果槽数量来自候选结果、目标玩法或 DiceSet 默认配置。 - 第一版 `rocket` 生成 3 个 `rocket` 展示槽。 - `rocket` 解析到 `slap_down_normal`。 - `thief` 解析到 `treasure_heist_normal`。 ### 链路测试 - 普通 Roll 出 `clover + clover` 后,最终进入 Lucky Dice,而不是发普通奖励。 - Lucky Dice 选中 `rocket` 后,启动 Slap Down 普通模式。 - Lucky Dice 选中 `thief` 后,启动 Treasure Heist 普通模式。 - 同一个 `RollSessionId` 能串起普通结果、命中规则、Lucky Dice 候选、随机结果和最终 ModeKey。 - 普通骰子数量从 2 改为 3 时,组合事实与 matcher 仍按统计结果工作,不出现固定左右骰逻辑。 ## 验收映射 | 需求验收点 | 规格落点 | | --- | --- | | 普通骰子结果能转换为结构化组合事实 | `ComboFacts`、组合事实测试 | | 普通骰子数量和 Lucky Dice 结果槽数量来自配置 | `DiceSetConfig`、`LuckyDiceResultSlots`、槽构建测试 | | 少量通用匹配器命中普通组合 | `ComboMatcherSpec`、规则匹配测试 | | 双四叶草触发 Lucky Dice | 普通规则优先级 100、链路测试 | | 执行层不维护大型组合 map | `IRollActionExecutor`、行为执行测试 | | Lucky Dice 先筛选再权重随机 | Lucky Dice 筛选、权重随机接口 | | Lucky Dice 结果解析到目标玩法和模式 | `TargetModeResolver`、模式映射 | | Rocket 进入 Slap Down 普通模式 | 第一版模式映射、Lucky Dice 测试 | | Thief 进入 Treasure Heist 普通模式 | 第一版模式映射、Lucky Dice 测试 | | 候选池为空时有可观测兜底 | 兜底与错误处理、日志字段 | | 日志能追踪一次 Roll 的关键决策 | 可观测性字段、链路测试 | ## 第一版不做 - 复杂动画完整复刻。 - 所有 Lucky Dice 特殊结果类型。 - 复杂运营活动权重策略。 - 完整短玩法内容实现。 - 远端配置热更新。 - 按组合 key 直接绑定业务方法。 ## 后续扩展建议 - 当组合规则增多后,把本地配置迁移到 ScriptableObject 或 JSON 配置表。 - 当 Lucky Dice 特殊结果超过第一版范围后,补充结果图标、目标玩法、默认模式和冷却规则的配置校验。 - 当接入埋点时,直接复用 `RollSessionId` 和可观测性字段,避免另起一套追踪链路。 - 当新增有序组合时,只新增规则声明与 matcher 参数,不修改 `ComboFacts` 的原始结果保存方式。 - 当普通骰子扩展到 3 颗或 4 颗时,优先新增统计型规则,不新增固定位置分支。