23 KiB
Lucky Dice 核心流程规格
文档信息
- 来源需求:lucky-dice-core-flow-requirements.md
- 适用范围:FishDice 第一版普通骰子主循环与 Lucky Dice 特殊分支
- 文档目标:把玩法需求拆成可实现、可测试、可扩展的系统规格
设计目标
Lucky Dice 核心流程需要支持两层玩法:
- 普通骰子主循环:玩家按骰子集合配置 Roll 普通骰子,根据组合事实匹配规则并执行奖励或特殊触发行为。
- Lucky Dice 特殊流程:当普通骰子出现双四叶草或配置声明的触发条件时,进入独立的特殊结果池,经过筛选、权重随机和模式解析后启动目标玩法。
第一版只做核心闭环,不实现复杂动画、远端热更、完整短玩法内容和复杂运营权重策略。系统边界必须保留扩展点,避免后续新增组合或特殊结果时不断堆硬编码分支。
第一版默认值:
- 普通骰子数量默认 2 颗,但必须来自 DiceSet 配置。
- Lucky Dice 结果槽数量默认 3 格,但必须来自 Lucky Dice 结果或目标玩法配置。
- 规则系统、组合事实和展示流程不得把 2 颗骰子或 3 格结果槽写死为不可扩展假设。
总体架构
核心模块按职责拆分为以下几层:
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 流程
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 第一版默认两颗骰子,骰子面为:
2 / 3 / 4 / 5 / 6 / Clover
骰子数量必须从 DiceSetConfig.DiceCount 读取。第一版配置值可以是 2,但 DiceRollService、ComboFactBuilder 和 matcher 都必须按 Faces.Count 或 DiceCount 处理,不读取固定的左骰、右骰两个位置。
Lucky Dice 流程
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 使用哪套骰子、投几颗骰子,以及该集合是否拥有展示槽概念。
public sealed class DiceSetConfig
{
public string DiceSetId { get; init; }
public DiceSetType SetType { get; init; }
public int DiceCount { get; init; }
public IReadOnlyList<DiceFaceDefinition> Faces { get; init; }
public int? DefaultResultSlotCount { get; init; }
}
约束:
DiceCount必须大于 0。- 第一版普通骰子
DiceCount = 2,但核心逻辑不得依赖该常量。 - Lucky Dice 如果不真实 Roll 多颗特殊骰,可以将
DiceCount理解为结果选择次数;第一版只随机一次,再由展示槽配置生成多个相同槽。 DefaultResultSlotCount用于展示层默认槽数,第一版 Lucky Dice 为 3。
DiceFace
普通骰子面与 Lucky Dice 特殊结果必须区分集合类型,不能混用同一个裸字符串列表。
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
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<DiceFace> Faces { get; init; }
public int Multiplier { get; init; }
public string TriggerSource { get; init; }
}
约束:
DiceCount必须等于本次配置要求的投掷数量。Faces.Count必须等于本次实际产出的骰子面数量。- 普通 Roll 中
DiceCount和Faces.Count不一致时,应拒绝进入规则匹配并记录错误。
ComboFacts
ComboFacts 是普通组合匹配的唯一输入。
public sealed class ComboFacts
{
public DiceSetType DiceSetType { get; init; }
public int DiceCount { get; init; }
public IReadOnlyList<DiceFace> Faces { get; init; }
public IReadOnlyDictionary<string, int> FaceCounts { get; init; }
public int NumberSum { get; init; }
public IReadOnlyList<int> 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
public sealed class ComboRule
{
public string RuleId { get; init; }
public int Priority { get; init; }
public ComboMatcherSpec Matcher { get; init; }
public IReadOnlyList<RollActionSpec> 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
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:
ExactComboAllNumbersContainsFaceFaceCountFaceCountRangeDiceCountNumberPairNumberOfAKindNumberSumRange
新增组合语义优先新增规则配置;只有出现新的通用匹配能力时才新增 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
public sealed class RollActionSpec
{
public string ActionType { get; init; }
public IReadOnlyDictionary<string, string> Params { get; init; }
}
首批 action:
grant_rewardadd_multipliertrigger_lucky_diceenter_modeshow_popup
执行层只按 ActionType 找执行器,不按组合 key 找方法。
LuckyDiceCandidate
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<string> 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 随机只选中一个特殊结果;展示层按槽配置生成实际槽位。
public sealed class LuckyDiceResultSlots
{
public string ResultKey { get; init; }
public string TargetKey { get; init; }
public int SlotCount { get; init; }
public IReadOnlyList<string> SlotResultKeys { get; init; }
}
第一版要求:
SlotCount默认 3。SlotResultKeys第一版全部填入同一个ResultKey。- 后续如某玩法需要不同槽位结果,可通过配置扩展
SlotResultKeys的生成策略。
LuckyDiceContext
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<string, object> RuntimeTags { get; init; }
}
TargetModeEntry
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; }
}
接口边界
组合事实构建
public interface IComboFactBuilder
{
ComboFacts Build(DiceRollResult result);
}
要求:
- 只接受普通骰子结果构建普通组合事实。
- 如果传入 Lucky Dice 结果,应返回错误或拒绝处理,避免两套骰子语义混用。
- 输出必须保留 DiceCount、原始 Faces、FaceCounts、NumberValues、NormalizedKey、OrderedKey。
- 不允许只读取固定两个骰子位置;必须遍历
Faces。
组合规则匹配
public interface IComboRuleMatcher
{
ComboRuleMatchResult Match(ComboFacts facts, IReadOnlyList<ComboRule> rules);
}
要求:
- matcher 由
MatcherType注册,不由组合 key 注册。 - 匹配结果需要包含命中的
RuleId、Priority、MatcherType和关键输入 facts,供日志追踪。
行为执行
public interface IRollActionExecutor
{
string ActionType { get; }
RollActionResult Execute(RollActionContext context, RollActionSpec action);
}
要求:
RollActionDispatcher只维护ActionType -> IRollActionExecutor注册表。- 不允许维护
clover_clover -> SomeMethod这类随组合数量增长的 map。 - 多个 action 按规则配置顺序执行。
- action 失败时需要返回结构化失败结果,并进入统一日志。
Lucky Dice 筛选
public interface ILuckyDiceCandidateFilter
{
string FilterType { get; }
IReadOnlyList<LuckyDiceCandidate> Apply(
IReadOnlyList<LuckyDiceCandidate> candidates,
LuckyDiceContext context);
}
首批 filter:
EnabledFilterProgressFilterSourceFilterCooldownFilterTutorialFilter
筛选器按固定顺序执行,顺序建议为:
Enabled → Progress → Source → Cooldown → Tutorial
权重随机
public interface IWeightedPicker
{
LuckyDiceCandidate Pick(
IReadOnlyList<LuckyDiceCandidate> candidates,
LuckyDiceContext context,
IRandomSource random);
}
要求:
- 权重小于等于 0 的候选不参与普通随机。
- 随机源可注入,便于测试和回放。
- 选中结果必须记录候选池快照、权重、随机值和最终 ResultKey。
Lucky Dice 结果槽构建
public interface ILuckyDiceResultSlotBuilder
{
LuckyDiceResultSlots BuildSlots(
LuckyDiceCandidate selected,
LuckyDiceContext context,
DiceSetConfig luckyDiceSetConfig);
}
要求:
- 槽数优先级:候选结果配置
ResultSlotCount→ 目标玩法配置 → Lucky Dice 集合配置DefaultResultSlotCount。 - 第一版每个槽都展示同一个
ResultKey。 - 槽数小于等于 0 时返回结构化错误并进入 Lucky Dice 兜底。
模式解析
public interface ITargetModeResolver
{
TargetModeEntry Resolve(LuckyDiceCandidate selected, LuckyDiceContext context);
}
解析优先级:
TutorialModeRuleBonusModeRuleDefaultModeRule
第一版映射:
| ResultKey | TargetKey | Default ModeKey |
|---|---|---|
rocket |
slap_down |
slap_down_normal |
thief |
treasure_heist |
treasure_heist_normal |
兜底与错误处理
普通组合未命中
处理策略:
- 记录
RollSessionId、原始 Faces、NormalizedKey。 - 使用配置的普通奖励兜底,或返回可见错误给调试面板。
- 不进入 Lucky Dice。
第一版建议默认发普通奖励兜底,避免玩家流程中断。
Lucky Dice 候选池为空
处理策略:
- 尝试使用配置的 default result。
- 如果 default result 不可用,执行普通奖励兜底。
- 记录空池原因:初始候选数量、每个 filter 过滤前后数量、最终兜底类型。
目标模式解析失败
处理策略:
- 记录
ResultKey、TargetKey、上下文标签。 - 如果存在 TargetKey 默认模式,降级到默认模式。
- 如果不存在默认模式,返回 Lucky Dice 失败并执行普通奖励兜底。
可观测性
每次 Roll 必须拥有同一个 RollSessionId,贯穿普通 Roll、规则匹配、Lucky Dice、模式解析和目标玩法启动。
最小日志字段:
RollSessionId
TriggerSource
NormalFaces
NormalDiceCount
ComboNormalizedKey
MatchedRuleId
MatchedRulePriority
ExecutedActions
LuckyDiceInitialCandidates
LuckyDiceFilteredCandidates
LuckyDiceFallbackType
LuckyDiceSelectedResultKey
LuckyDiceSelectedTargetKey
LuckyDiceResultSlotCount
LuckyDiceSlotResultKeys
ResolvedModeKey
FinalOutcome
日志目标:
- 复盘一次 Roll 为什么发奖、为什么进 Lucky Dice、为什么进某个玩法。
- 发现配置问题时能看出是规则未命中、候选池为空、权重异常还是模式解析失败。
配置示例
第一版可以使用本地静态配置或 ScriptableObject;远端配置热更新不在范围内。
骰子集合配置示例
{
"diceSetId": "normal_main",
"setType": "Normal",
"diceCount": 2,
"faces": ["2", "3", "4", "5", "6", "clover"]
}
{
"diceSetId": "lucky_dice",
"setType": "Lucky",
"diceCount": 1,
"defaultResultSlotCount": 3,
"faces": ["rocket", "thief", "chest", "bomb", "key"]
}
普通组合规则示例
[
{
"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
}
]
后续多骰规则示例:
[
{
"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 候选示例
[
{
"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 颗时,优先新增统计型规则,不新增固定位置分支。