Files
Fishdice/FishDice/Docs/Specs/lucky-dice-core-flow-spec.md
2026-06-23 14:13:08 +08:00

23 KiB
Raw Permalink Blame History

Lucky Dice 核心流程规格

文档信息

  • 来源需求: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 格结果槽写死为不可扩展假设。

总体架构

核心模块按职责拆分为以下几层:

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 读取。第一版配置值可以是 2DiceRollServiceComboFactBuilder 和 matcher 都必须按 Faces.CountDiceCount 处理,不读取固定的左骰、右骰两个位置。

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,例如 rocketthief
  • 业务代码不得到处裸写这些 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 中 DiceCountFaces.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 + cloverclover + 2 都归一为 2_clover
  • clover + clover 归一为 clover_clover
  • N 颗骰子的 NormalizedKey 由所有面 key 排序后拼接,例如 2_2_clover3_3_3
  • 后续若需要有序语义,由规则显式声明使用 OrderedKey

约束:

  • ComboFacts 不能假设只有 2 颗骰子。
  • matcher 必须基于 DiceCountFacesFaceCountsNumberValues 等统计信息判断。
  • 只有有序规则才能读取 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

  • 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

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

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 注册。
  • 匹配结果需要包含命中的 RuleIdPriorityMatcherType 和关键输入 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

  • EnabledFilter
  • ProgressFilter
  • SourceFilter
  • CooldownFilter
  • TutorialFilter

筛选器按固定顺序执行,顺序建议为:

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);
}

解析优先级:

  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. 记录 ResultKeyTargetKey、上下文标签。
  2. 如果存在 TargetKey 默认模式,降级到默认模式。
  3. 如果不存在默认模式,返回 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 = 5CloverCount = 0NormalizedKey = "2_3"
  • 2 + cloverclover + 2 都生成 NormalizedKey = "2_clover"
  • clover + clover 生成 CloverCount = 2NormalizedKey = "clover_clover"
  • 2 + 2 + clover 生成 DiceCount = 3NumberValues = [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 结果槽数量来自配置 DiceSetConfigLuckyDiceResultSlots、槽构建测试
少量通用匹配器命中普通组合 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 颗时,优先新增统计型规则,不新增固定位置分支。