766 lines
23 KiB
Markdown
766 lines
23 KiB
Markdown
# 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<DiceFaceDefinition> 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<DiceFace> 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<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
|
||
|
||
```csharp
|
||
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
|
||
|
||
```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<string, string> 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<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 随机只选中一个特殊结果;展示层按槽配置生成实际槽位。
|
||
|
||
```csharp
|
||
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
|
||
|
||
```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<string, object> 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<ComboRule> 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<LuckyDiceCandidate> Apply(
|
||
IReadOnlyList<LuckyDiceCandidate> candidates,
|
||
LuckyDiceContext context);
|
||
}
|
||
```
|
||
|
||
首批 filter:
|
||
|
||
- `EnabledFilter`
|
||
- `ProgressFilter`
|
||
- `SourceFilter`
|
||
- `CooldownFilter`
|
||
- `TutorialFilter`
|
||
|
||
筛选器按固定顺序执行,顺序建议为:
|
||
|
||
```text
|
||
Enabled → Progress → Source → Cooldown → Tutorial
|
||
```
|
||
|
||
### 权重随机
|
||
|
||
```csharp
|
||
public interface IWeightedPicker
|
||
{
|
||
LuckyDiceCandidate Pick(
|
||
IReadOnlyList<LuckyDiceCandidate> 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 颗时,优先新增统计型规则,不新增固定位置分支。
|