diff --git a/analysis/FishDice/index.html b/analysis/FishDice/index.html
index 53cd15d..7cc3f9f 100644
--- a/analysis/FishDice/index.html
+++ b/analysis/FishDice/index.html
@@ -19,6 +19,7 @@
结构与分层Runtime、Demo、DemoEditor、Tests 的边界与依赖方向。
流程与生命周期从 Roll 请求到奖励、Lucky Dice、目标玩法入口的完整链路。
+
随机逻辑解释权重随机、分层池漏斗、状态沉淀与可观测 Trace 的决策图。
玩法与模块开发规则、Matcher、Action、候选、TargetMode 如何扩展。
资源管理当前资源策略、配置形态、Unity 资源接入缺口。
UI 管理Demo UI 的组织方式,以及框架层尚未拥有的 UI 抽象。
diff --git a/analysis/FishDice/random-logic.html b/analysis/FishDice/random-logic.html
new file mode 100644
index 0000000..5f3ed7e
--- /dev/null
+++ b/analysis/FishDice/random-logic.html
@@ -0,0 +1,151 @@
+
+
+
+
+
+
FishDice 随机逻辑解释
+
+
+
+
+
+
+
+ FishDice 随机逻辑:从权重候选到分层池漏斗
+ 这页解释当前仓库里两套随机决策:一套服务 Lucky Dice 候选选择,另一套是更通用的 Pool Funnel 分层随机池。它们共同的设计目标是:随机可注入、权重可解释、结果可回放、失败可观测。
+
+
+
+ 一句话模型
+ 随机并不是“从列表里随手挑一个”。框架会先筛候选、算权重、只在最高优先级集合里抽取,再把随机值、选中项、状态变化和后续决策写入结果对象或 Trace。
+
+
IRandomSource
随机源由外部注入,测试可以用固定序列复现结果。
+
Weight
候选通过权重参与抽取,权重为 0 的项不会进入抽奖池。
+
Trace
Pool Funnel 会记录每层权重、随机值、命中结果、状态前后差异。
+
+
+
+
+ 流程图:Pool Funnel 随机决策
+
+
+
+ 1. 输入请求
+ RandomPoolDefinition + PoolFunnelRequest 进入服务。
+
+
+ 2. 解析入口层
+ 检查 EntryLayerIds,过滤不可用层,按 priority 选入口。
+
+
+ 3. 读取状态
+ 从 IRandomPoolStateStore 取当前层 luck value 和 roll count。
+
+
+ 4. 计算权重
+ 默认公式:BaseWeight + Factor * LuckValue,小于 0 归零。
+
+
+ 5. 筛候选池
+ 只保留权重大于 0 的结果,再取最高 priority 的结果组。
+
+
+ 6. 权重抽取
+ Next(0, totalWeight) 得到随机游标,累加权重命中结果。
+
+
+ 7. 执行动作
+ 把选中结果声明的 actions 交给 dispatcher,默认只排队成功结果。
+
+
+ 8. 沉淀状态
+ 默认将 LuckValue += LuckValueDelta,并让 roll count +1。
+
+
+ 9. 判断去向
+ 结果配置决定 Stop 或 NextLayer;NextLayer 会进入下一层循环。
+
+
+ 10. 输出结果
+ 返回 outcome、每层详情和 PoolFunnelTrace,便于复盘。
+
+
+
+ 这个流程的关键不是“随机算法多复杂”,而是把随机前后的上下文都保留下来:入口层为什么可用、候选为什么被过滤、某个结果为什么权重更高、抽中的随机游标落在哪个区间。
+
+
+
+ 框架图:随机池组件关系
+
+
+
+
配置与输入
+
RandomPoolDefinition定义 pool id、入口层列表、所有随机层。
+
RandomLayerDefinition定义 layer id、优先级、可用性 key、结果集合。
+
RandomResultDefinition定义 result key、priority、基础权重、幸运值因子、状态增量和下一步决策。
+
+
+
+
核心编排
+
PoolFunnelRandomService负责入口层解析、循环 RollLayer、处理空池、处理 NextLayer、生成最终结果。
+
RollLayer读取状态 → 算全部权重 → 取最高优先级候选 → 权重抽取 → action → settlement → decision。
+
PoolFunnelRunResult输出 session、pool、每层结果、最终 outcome 和可回放 Trace。
+
+
+
+
可替换策略
+
IRandomSource控制随机值来源,线上用真实随机,测试用脚本随机。
+
ILayerAvailabilityPolicy判断层是否开放,默认 AlwaysAvailable。
+
IResultWeightCalculator计算结果权重,默认线性幸运值公式。
+
IPoolSettlementStrategy处理抽中后的状态变化,默认按 delta 修改幸运值。
+
IFunnelDecisionStrategy决定停在当前结果,还是进入下一层。
+
+
+
+
+
+
+ 核心公式
+ 默认权重计算来自 LinearLuckValueWeightCalculator:
+ weight = max(0, BaseWeight + LuckValueWeightFactor * LuckValue)
+ 抽取时先求最高优先级候选的总权重,然后生成 [0, totalWeight) 的随机数。遍历候选并累加权重,随机数落入哪个区间,就选中哪个结果。
+ priority 比 weight 更先发生作用:低优先级结果即使权重很高,也不会和最高优先级结果同池竞争。
+
+
+
+ Lucky Dice 候选随机
+
+
先过滤。只保留 enabled、满足 player level、满足 trigger source 的候选。
+
再按权重抽取。权重大于 0 的候选进入池子,使用同样的累加区间方式命中结果。
+
最后兜底。如果没有可抽候选,优先找 default candidate;仍没有时返回 NormalRewardFallback。
+
补展示槽。选中候选后决定 result slot count,候选未配置时使用 Lucky Dice 骰子集默认槽数。
+
+ 因此 Lucky Dice 的随机逻辑更轻:它没有多层状态沉淀,也不循环跳层;它主要服务“从当前可用特殊结果里选一个入口”。
+
+
+
+ 为什么这样设计
+
+ | 问题 | 设计回答 | 收益 |
+ | 测试里怎么复现随机? | 随机源走 IRandomSource 注入。 | 固定随机序列即可断言选中项。 |
+ | 概率为什么变化? | 权重计算器显式依赖基础权重和幸运值。 | 策划参数、保底逻辑、状态变化都能解释。 |
+ | 结果池为空怎么办? | Pool Funnel 返回 NoWeightedResult;Lucky Dice 走 default 或普通奖励兜底。 | 失败路径不会静默吞掉。 |
+ | 以后要换概率算法怎么办? | 可替换 weight calculator、settlement、decision strategy。 | 核心编排稳定,策略可以独立演进。 |
+
+
+
+
+ 代码证据
+
+ - PoolFunnelRandomService.cs:52-88:入口层解析、循环滚层和 NextLayer 处理。
+ - PoolFunnelRandomService.cs:117-169:权重计算、最高优先级候选、随机游标、状态沉淀和决策。
+ - PoolFunnelStrategies.cs:35-74:默认可用性、线性权重和 delta settlement。
+ - LuckyDiceFlowService.cs:16-49:Lucky Dice 候选过滤、权重选择、默认结果兜底。
+ - LuckyDiceRollFlowService.cs:65-90:只有 action 产生
LuckyDiceRequestedEffect 时才进入 Lucky Dice 随机选择。
+
+
+
+
+
+
+
diff --git a/analysis/FishDice/style.css b/analysis/FishDice/style.css
index feab670..9cdd215 100644
--- a/analysis/FishDice/style.css
+++ b/analysis/FishDice/style.css
@@ -281,6 +281,152 @@ pre {
font-size: 14px;
}
+.diagram-panel {
+ border: 1px solid var(--line);
+ background:
+ linear-gradient(135deg, rgba(184, 71, 42, 0.08), transparent 36%),
+ linear-gradient(315deg, rgba(31, 111, 104, 0.08), transparent 34%),
+ var(--panel);
+ padding: 20px;
+ margin: 18px 0 28px;
+ overflow-x: auto;
+}
+
+.logic-flow {
+ display: grid;
+ grid-template-columns: repeat(5, minmax(142px, 1fr));
+ gap: 14px;
+ min-width: 820px;
+ align-items: stretch;
+}
+
+.logic-node {
+ position: relative;
+ border: 2px solid var(--line);
+ background: rgba(255, 253, 247, 0.92);
+ padding: 14px;
+ min-height: 112px;
+}
+
+.logic-node::after {
+ content: "→";
+ position: absolute;
+ right: -18px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--accent);
+ font-size: 22px;
+ font-weight: 800;
+}
+
+.logic-node:last-child::after,
+.logic-node.break::after {
+ display: none;
+}
+
+.logic-node strong {
+ display: block;
+ color: var(--accent);
+ margin-bottom: 6px;
+}
+
+.logic-node span {
+ display: block;
+ color: var(--muted);
+ font-size: 13px;
+ line-height: 1.55;
+}
+
+.logic-node.alt {
+ border-color: rgba(31, 111, 104, 0.62);
+}
+
+.logic-node.stop {
+ border-color: rgba(157, 44, 47, 0.56);
+}
+
+.framework-map {
+ display: grid;
+ grid-template-columns: 1fr 1.25fr 1fr;
+ gap: 16px;
+ min-width: 900px;
+}
+
+.framework-column {
+ display: grid;
+ gap: 12px;
+ align-content: start;
+}
+
+.framework-title {
+ font-weight: 800;
+ color: var(--accent-2);
+ padding-bottom: 8px;
+ border-bottom: 2px solid var(--line);
+}
+
+.framework-box {
+ border: 1px solid var(--line);
+ background: rgba(255, 253, 247, 0.9);
+ padding: 14px;
+}
+
+.framework-box strong {
+ display: block;
+ color: var(--accent);
+ margin-bottom: 4px;
+}
+
+.framework-box p {
+ margin: 0;
+ color: var(--muted);
+ font-size: 13px;
+ line-height: 1.55;
+}
+
+.formula {
+ display: inline-block;
+ margin: 6px 0;
+ padding: 10px 12px;
+ border: 1px solid var(--line);
+ background: var(--soft);
+ font-family: Consolas, "Cascadia Mono", monospace;
+ color: var(--code);
+}
+
+.step-list {
+ counter-reset: step;
+ display: grid;
+ gap: 12px;
+ margin: 18px 0;
+}
+
+.step-item {
+ counter-increment: step;
+ border: 1px solid var(--line);
+ background: var(--panel);
+ padding: 14px 16px 14px 54px;
+ position: relative;
+}
+
+.step-item::before {
+ content: counter(step);
+ position: absolute;
+ left: 14px;
+ top: 14px;
+ width: 26px;
+ height: 26px;
+ display: grid;
+ place-items: center;
+ background: var(--accent-2);
+ color: white;
+ font-weight: 800;
+}
+
+.step-item strong {
+ color: var(--accent);
+}
+
@media (max-width: 760px) {
.shell {
width: min(100vw - 24px, 1180px);
@@ -294,4 +440,8 @@ pre {
.flow-row {
grid-template-columns: 1fr;
}
+
+ .diagram-panel {
+ padding: 14px;
+ }
}