feat(lucky-dice): 提交 demo 前核心流程
This commit is contained in:
20
FishDice/Docs/Decisions/README.md
Normal file
20
FishDice/Docs/Decisions/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Lucky Dice 架构决策记录
|
||||
|
||||
本目录记录 Lucky Dice 核心流程第一版中的重要架构决策。ADR 用来保存当时的背景、约束、备选方案和取舍结果,避免后续实现或扩展时重新讨论已经明确的架构边界。
|
||||
|
||||
## ADR 列表
|
||||
|
||||
| 编号 | 决策 | 状态 |
|
||||
| --- | --- | --- |
|
||||
| [ADR-001](adr-001-separate-normal-and-lucky-dice-sets.md) | 普通骰子和 Lucky Dice 特殊结果分成两套集合 | Accepted |
|
||||
| [ADR-002](adr-002-build-combo-facts-before-rule-matching.md) | Roll 结果先转组合事实,再匹配规则 | Accepted |
|
||||
| [ADR-003](adr-003-dispatch-actions-by-action-type.md) | 行为按 action type 注册和分发 | Accepted |
|
||||
| [ADR-004](adr-004-inject-random-source-for-lucky-dice.md) | Lucky Dice 权重随机使用可注入随机源 | Accepted |
|
||||
|
||||
## 状态说明
|
||||
|
||||
- `Accepted`:当前采用的决策。
|
||||
- `Superseded by ADR-XXX`:已经被后续 ADR 替代。
|
||||
- `Deprecated`:不再推荐,但未被某个明确新 ADR 替代。
|
||||
|
||||
ADR 不应删除。决策变化时新增一份 ADR,并在旧 ADR 的状态里标记替代关系。
|
||||
@@ -0,0 +1,116 @@
|
||||
# ADR-001: 普通骰子和 Lucky Dice 特殊结果分成两套集合
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-22
|
||||
|
||||
## Context
|
||||
|
||||
FishDice 第一版需要同时支持普通骰子主循环和 Lucky Dice 特殊分支。普通 Roll 的骰子面是 `2 / 3 / 4 / 5 / 6 / clover`,它们的职责是参与组合事实、规则匹配、普通奖励、Clover bonus 和触发 Lucky Dice。
|
||||
|
||||
Lucky Dice 进入后使用的是特殊结果,例如 `rocket`、`thief`。这些结果的职责不是点数结算,而是决定后续进入哪个短玩法,以及进入该玩法的哪个模式。
|
||||
|
||||
如果两类结果混用同一套裸字符串或同一个骰子集合,会带来几个风险:
|
||||
|
||||
- 普通组合匹配器可能错误处理 Lucky Dice 特殊结果。
|
||||
- 策划配置容易把数字奖励结果和玩法入口结果混在一起。
|
||||
- 日志中很难判断某个 key 是普通骰子面还是 Lucky Dice 入口。
|
||||
- 后续扩展更多特殊结果时,会污染普通 Roll 的规则空间。
|
||||
|
||||
## Decision
|
||||
|
||||
普通骰子和 Lucky Dice 特殊结果使用两套独立集合,并在数据模型中保留集合类型。
|
||||
|
||||
建议在代码中使用等价于以下概念的结构:
|
||||
|
||||
```csharp
|
||||
public enum DiceSetType
|
||||
{
|
||||
Normal,
|
||||
Lucky
|
||||
}
|
||||
|
||||
public readonly struct DiceFace
|
||||
{
|
||||
public DiceSetType SetType { get; }
|
||||
public string Key { get; }
|
||||
public int? NumberValue { get; }
|
||||
}
|
||||
```
|
||||
|
||||
普通骰子集合只包含主循环 Roll 面。Lucky Dice 集合只包含特殊入口结果。普通组合事实构建器只接受 `DiceSetType.Normal` 的 Roll 结果,遇到 Lucky Dice 特殊结果时必须拒绝或返回结构化错误。
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 使用同一个 DiceFace 字符串集合
|
||||
|
||||
优点:
|
||||
|
||||
- 第一版实现最省事。
|
||||
- 配置表字段少。
|
||||
|
||||
缺点:
|
||||
|
||||
- `clover`、`rocket`、`thief`、数字面都会落在同一个匹配空间。
|
||||
- 普通 matcher 需要不断写排除逻辑,防止特殊结果进入普通奖励结算。
|
||||
- 日志和调试工具无法从类型上识别语义边界。
|
||||
|
||||
结论:拒绝。它会把第一版的简单实现成本转化成后续规则扩展和排查成本。
|
||||
|
||||
### 使用同一个集合,但通过 key 前缀区分
|
||||
|
||||
示例:`normal_2`、`normal_clover`、`lucky_rocket`。
|
||||
|
||||
优点:
|
||||
|
||||
- 比裸字符串更可读。
|
||||
- 不需要额外枚举字段。
|
||||
|
||||
缺点:
|
||||
|
||||
- 类型边界依赖命名约定,配置错误时很难在编译或校验阶段发现。
|
||||
- matcher 仍然会看到同一批 key,需要人工遵守前缀规则。
|
||||
- 后续如果出现多套普通骰子或多套特殊结果,前缀会越来越复杂。
|
||||
|
||||
结论:拒绝。前缀可以作为日志展示或配置命名辅助,但不能替代结构化集合类型。
|
||||
|
||||
### 使用两套独立数据模型
|
||||
|
||||
普通骰子面和 Lucky Dice 特殊结果完全使用不同 class。
|
||||
|
||||
优点:
|
||||
|
||||
- 类型隔离最强。
|
||||
- 不容易误用。
|
||||
|
||||
缺点:
|
||||
|
||||
- Roll、展示槽、日志等通用结构需要重复建模。
|
||||
- 第一版通用流程会变得偏重。
|
||||
|
||||
结论:暂不采用。当前使用共同的 `DiceFace` 概念加 `DiceSetType` 已能满足隔离需求。
|
||||
|
||||
## Consequences
|
||||
|
||||
正向影响:
|
||||
|
||||
- 普通 Roll 和 Lucky Dice 的语义边界清晰。
|
||||
- 普通组合 matcher 不需要理解特殊玩法入口。
|
||||
- 日志可以明确记录本次处理的是 `Normal` 还是 `Lucky` 集合。
|
||||
- 后续新增特殊结果不会污染普通奖励规则。
|
||||
|
||||
代价和约束:
|
||||
|
||||
- 配置工具和调试界面必须展示集合类型。
|
||||
- 配置加载需要校验普通集合不能包含 Lucky Dice 特殊结果。
|
||||
- 代码中不能到处裸写 key,需要集中定义 key 或配置入口。
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Lucky Dice 核心流程 PRD](../PRD/lucky-dice-core-flow-prd.md)
|
||||
- [Lucky Dice 核心流程规格](../Specs/lucky-dice-core-flow-spec.md)
|
||||
- [Lucky Dice 核心流程架构蓝图](../Architecture/lucky-dice-core-flow-architecture-blueprint.md)
|
||||
@@ -0,0 +1,117 @@
|
||||
# ADR-002: Roll 结果先转组合事实,再匹配规则
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-22
|
||||
|
||||
## Context
|
||||
|
||||
Lucky Dice 第一版普通 Roll 默认使用 2 颗骰子,但 PRD 明确要求骰子数量必须来自配置,并保留后续扩展到 3 颗、4 颗或特殊关卡自定义骰子数量的能力。
|
||||
|
||||
普通 Roll 的规则也不只包含精确组合。首批规则需要覆盖:
|
||||
|
||||
- 双 Clover 触发 Lucky Dice。
|
||||
- 单 Clover 加数字发放普通奖励并提供 bonus。
|
||||
- 纯数字结果发放普通奖励。
|
||||
|
||||
后续还需要支持对子、N 连、指定面数量、点数和区间、骰子数量约束等统计型规则。
|
||||
|
||||
如果规则匹配直接读取固定位置,例如左骰和右骰,就会把第一版默认值 `2` 固化到系统里。后续扩展 3 骰时,规则、测试和日志都要大范围修改。
|
||||
|
||||
## Decision
|
||||
|
||||
普通 Roll 结果必须先转换成结构化 `ComboFacts`,再进入规则匹配。
|
||||
|
||||
`ComboFacts` 至少包含:
|
||||
|
||||
- `DiceSetType`
|
||||
- `DiceCount`
|
||||
- 原始 `Faces`
|
||||
- `FaceCounts`
|
||||
- `NumberValues`
|
||||
- `NumberSum`
|
||||
- `CloverCount`
|
||||
- `HasClover`
|
||||
- `NormalizedKey`
|
||||
- `OrderedKey`
|
||||
|
||||
普通规则默认使用无序事实。`2 + clover` 与 `clover + 2` 必须归一为同一个 `NormalizedKey`。只有规则明确声明需要顺序语义时,才允许读取 `OrderedKey`。
|
||||
|
||||
matcher 必须基于 `ComboFacts` 中的统计信息判断,不能依赖固定的左骰、右骰字段。
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 在规则里直接读取骰子数组位置
|
||||
|
||||
示例:`faces[0] == clover && faces[1] == clover`。
|
||||
|
||||
优点:
|
||||
|
||||
- 第一版双骰场景写起来最快。
|
||||
- 不需要单独的事实构建层。
|
||||
|
||||
缺点:
|
||||
|
||||
- 默认写死 2 骰。
|
||||
- 顺序和无序语义容易混乱。
|
||||
- 后续 3 骰、4 骰会导致规则分支爆炸。
|
||||
- 测试会绑定实现细节,而不是外部行为。
|
||||
|
||||
结论:拒绝。它与可配置骰子数量的核心要求冲突。
|
||||
|
||||
### 只生成字符串组合 key
|
||||
|
||||
示例:直接生成 `2_clover`、`clover_clover`,规则只匹配 key。
|
||||
|
||||
优点:
|
||||
|
||||
- 对精确组合匹配足够简单。
|
||||
- 配置和日志易读。
|
||||
|
||||
缺点:
|
||||
|
||||
- 难以表达统计型规则,例如至少 2 个 Clover、三连、点数和区间。
|
||||
- matcher 会被迫解析字符串,或者需要维护大量派生 key。
|
||||
- 原始骰子和统计信息缺失,不利于调试。
|
||||
|
||||
结论:拒绝作为唯一事实来源。`NormalizedKey` 可以作为 `ComboFacts` 的一个字段,但不能替代完整事实。
|
||||
|
||||
### 由每个 matcher 自己计算统计信息
|
||||
|
||||
优点:
|
||||
|
||||
- 不需要统一事实模型。
|
||||
- matcher 内部可以自由处理自己的需求。
|
||||
|
||||
缺点:
|
||||
|
||||
- 多个 matcher 重复遍历和统计。
|
||||
- 不同 matcher 对 key、顺序、数字值的理解可能不一致。
|
||||
- 日志难以记录统一的匹配输入。
|
||||
|
||||
结论:拒绝。统一事实模型更利于一致性和测试。
|
||||
|
||||
## Consequences
|
||||
|
||||
正向影响:
|
||||
|
||||
- 后续扩展骰子数量时,核心规则仍基于统计事实工作。
|
||||
- 规则匹配器可以用少量通用 matcher 覆盖多种组合语义。
|
||||
- 日志能记录原始结果、统计结果、无序 key 和有序 key。
|
||||
- 测试可以直接验证外部行为和事实结构。
|
||||
|
||||
代价和约束:
|
||||
|
||||
- 第一版需要实现并测试 `ComboFactBuilder`。
|
||||
- `ComboFacts` 字段需要保持稳定,不能随意为某个规则塞临时字段。
|
||||
- Lucky Dice 特殊结果不能进入普通 `ComboFactBuilder`。
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Lucky Dice 核心流程需求](../Requirements/lucky-dice-core-flow-requirements.md)
|
||||
- [Lucky Dice 核心流程规格](../Specs/lucky-dice-core-flow-spec.md)
|
||||
- [ADR-001](adr-001-separate-normal-and-lucky-dice-sets.md)
|
||||
@@ -0,0 +1,116 @@
|
||||
# ADR-003: 行为按 action type 注册和分发
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-22
|
||||
|
||||
## Context
|
||||
|
||||
PRD 要求普通骰子的不同组合能触发不同业务行为,并且后续可以新增组合、骰子数量、特殊结果或目标玩法。第一版最小组合包括:
|
||||
|
||||
- 数字 + 数字:普通奖励。
|
||||
- 数字 + Clover:普通奖励加 Clover bonus。
|
||||
- Clover + Clover:触发 Lucky Dice。
|
||||
|
||||
这些组合行为本质上由少量通用动作组合而成,例如发奖、增加倍率、触发 Lucky Dice、进入玩法模式、显示弹窗。
|
||||
|
||||
如果为每个组合 key 绑定一个独立方法,第一版可以很快写完,但后续新增组合时执行层会不断膨胀,形成难以维护的分支表。
|
||||
|
||||
## Decision
|
||||
|
||||
规则配置只声明要执行的 action 列表。执行层通过 `ActionType` 注册和分发通用 executor。
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
grant_reward -> GrantRewardExecutor
|
||||
add_multiplier -> AddMultiplierExecutor
|
||||
trigger_lucky_dice -> TriggerLuckyDiceExecutor
|
||||
enter_mode -> EnterModeExecutor
|
||||
show_popup -> ShowPopupExecutor
|
||||
```
|
||||
|
||||
`RollActionDispatcher` 只能维护 `ActionType -> IRollActionExecutor` 注册表,不能维护 `combo key -> method` 注册表。
|
||||
|
||||
组合规则增长时,优先新增或修改规则配置。只有出现新的通用行为类型时,才新增 executor。
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 每个组合 key 绑定独立方法
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
clover_clover -> TriggerLuckyDice()
|
||||
number_clover -> GrantCloverBonus()
|
||||
number_number -> GrantNormalReward()
|
||||
```
|
||||
|
||||
优点:
|
||||
|
||||
- 第一版逻辑直观。
|
||||
- 单个组合方法容易断点调试。
|
||||
|
||||
缺点:
|
||||
|
||||
- 组合数量增长会直接推动方法数量增长。
|
||||
- 相同行为会在多个方法里重复。
|
||||
- 策划新增组合含义时,工程通常必须新增方法。
|
||||
- 很难支持一个组合同时执行多个通用行为。
|
||||
|
||||
结论:拒绝。它正是 PRD 中要求避免的硬编码分支。
|
||||
|
||||
### 规则 matcher 直接执行业务行为
|
||||
|
||||
优点:
|
||||
|
||||
- 少一层 dispatcher。
|
||||
- matcher 命中后可以立刻执行。
|
||||
|
||||
缺点:
|
||||
|
||||
- matcher 同时承担判断和副作用,测试困难。
|
||||
- 同一个 matcher 无法复用于不同业务动作。
|
||||
- 规则优先级、继续匹配、行为顺序会混在一起。
|
||||
|
||||
结论:拒绝。匹配和执行必须分离。
|
||||
|
||||
### 用事件系统广播所有命中结果
|
||||
|
||||
优点:
|
||||
|
||||
- 行为扩展灵活。
|
||||
- 多个系统可以监听同一个结果。
|
||||
|
||||
缺点:
|
||||
|
||||
- 第一版流程需要确定性和可追踪,广播式事件容易让执行顺序不透明。
|
||||
- 兜底、失败和链路测试会更复杂。
|
||||
- 当前没有足够多的跨系统监听需求。
|
||||
|
||||
结论:暂不采用。后续如接入更多外围系统,可以在 executor 内部再发布领域事件,但核心流程仍由 action list 驱动。
|
||||
|
||||
## Consequences
|
||||
|
||||
正向影响:
|
||||
|
||||
- 新增组合多数情况下只改规则配置。
|
||||
- 通用行为可以复用和组合。
|
||||
- 链路测试可以断言 action list 和最终 outcome。
|
||||
- 未注册 action 可以返回结构化错误并写入追踪。
|
||||
|
||||
代价和约束:
|
||||
|
||||
- action 参数必须有校验,避免配置错误运行到一半才失败。
|
||||
- executor 的职责边界需要清楚,不能把组合判断重新塞回 executor。
|
||||
- dispatcher 必须保留执行顺序和失败策略。
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Lucky Dice 核心流程 PRD](../PRD/lucky-dice-core-flow-prd.md)
|
||||
- [Lucky Dice 核心流程规格](../Specs/lucky-dice-core-flow-spec.md)
|
||||
- [ADR-002](adr-002-build-combo-facts-before-rule-matching.md)
|
||||
@@ -0,0 +1,113 @@
|
||||
# ADR-004: Lucky Dice 权重随机使用可注入随机源
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-22
|
||||
|
||||
## Context
|
||||
|
||||
Lucky Dice 需要在候选结果池筛选后按权重随机选择一个结果。第一版至少需要支持:
|
||||
|
||||
- `rocket` 进入 `slap_down_normal`。
|
||||
- `thief` 进入 `treasure_heist_normal`。
|
||||
- 候选结果可按启用状态、玩家进度、触发来源、冷却状态和新手期规则筛选。
|
||||
- 候选池为空时有可观测兜底。
|
||||
|
||||
测试策略要求固定随机源下的权重随机结果可预测。PRD 还要求每次 Roll 都能通过同一个 `RollSessionId` 串起普通 Roll、规则匹配、Lucky Dice 结果和目标玩法启动,方便排查和回放。
|
||||
|
||||
如果 Lucky Dice 流程直接调用 Unity 或系统随机,会让链路测试不稳定,也难以复盘线上问题。
|
||||
|
||||
## Decision
|
||||
|
||||
Lucky Dice 权重随机必须通过可注入随机源执行。
|
||||
|
||||
建议使用等价于以下概念的接口:
|
||||
|
||||
```csharp
|
||||
public interface IRandomSource
|
||||
{
|
||||
int Range(int minInclusive, int maxExclusive);
|
||||
float Value01();
|
||||
}
|
||||
```
|
||||
|
||||
`WeightedPicker` 接收筛选后的候选列表、上下文和随机源。它需要记录:
|
||||
|
||||
- 候选列表快照。
|
||||
- 每个候选的有效权重。
|
||||
- 随机值。
|
||||
- 最终选中的 `ResultKey`。
|
||||
|
||||
测试环境可以注入固定序列随机源。运行时环境可以注入 Unity 随机适配器或项目统一随机服务。
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 直接使用 UnityEngine.Random
|
||||
|
||||
优点:
|
||||
|
||||
- Unity 项目中最容易使用。
|
||||
- 不需要额外接口。
|
||||
|
||||
缺点:
|
||||
|
||||
- 测试难以稳定复现。
|
||||
- 多个系统共享全局随机状态,结果容易受调用顺序影响。
|
||||
- 回放和问题定位难度高。
|
||||
|
||||
结论:拒绝在核心逻辑中直接使用。可以通过适配器在运行时接入 Unity 随机。
|
||||
|
||||
### 直接使用 System.Random
|
||||
|
||||
优点:
|
||||
|
||||
- 可指定 seed。
|
||||
- 不依赖 Unity API,适合 EditMode 测试。
|
||||
|
||||
缺点:
|
||||
|
||||
- 如果在核心服务内部自行 new,仍然无法由测试控制。
|
||||
- seed 生命周期和 RollSessionId 的关系不透明。
|
||||
- 后续如果项目已有统一随机服务,需要再改接口。
|
||||
|
||||
结论:不直接在核心流程中创建。可以作为 `IRandomSource` 的一个实现。
|
||||
|
||||
### 配置固定结果,不做随机
|
||||
|
||||
优点:
|
||||
|
||||
- 第一版实现和测试最简单。
|
||||
- 结果完全可控。
|
||||
|
||||
缺点:
|
||||
|
||||
- 不满足 Lucky Dice 候选结果支持权重的需求。
|
||||
- 后续接入概率策略时需要重构核心流程。
|
||||
- 无法提前验证候选筛选和权重随机之间的边界。
|
||||
|
||||
结论:拒绝。第一版可以配置极简权重,但流程必须保留权重随机。
|
||||
|
||||
## Consequences
|
||||
|
||||
正向影响:
|
||||
|
||||
- 链路测试可以稳定断言 Lucky Dice 最终结果。
|
||||
- 回放和问题排查可以复用相同随机输入。
|
||||
- `WeightedPicker` 不依赖 Unity API,更容易做纯 C# 测试。
|
||||
- 后续接入活动概率策略时,随机入口清晰。
|
||||
|
||||
代价和约束:
|
||||
|
||||
- 需要实现至少两个随机源:运行时随机源和测试固定随机源。
|
||||
- 需要记录随机值和候选权重,否则可注入随机源的排查价值会下降。
|
||||
- 随机源生命周期需要统一管理,避免同一 Roll 中不同阶段使用不同来源。
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Lucky Dice 核心流程 PRD](../PRD/lucky-dice-core-flow-prd.md)
|
||||
- [Lucky Dice 核心流程规格](../Specs/lucky-dice-core-flow-spec.md)
|
||||
- [ADR-003](adr-003-dispatch-actions-by-action-type.md)
|
||||
Reference in New Issue
Block a user