Files
90CNServer/server/internal/services/dnfbridge/tarot_lottery_roll_test.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

112 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dnfbridge
import (
"testing"
)
// 现场 2026-10-03(服主报「塔罗牌用不了」,角色 1 礼白666):连点 6 次,6 次的拒绝
// 原因**逐字相同**,都是奖池的第一项:
//
// reason=random reward item requires a complete runtime-PVF outcome table:
// item=108040415 kind="equipment" target_list=0 slots=9..64
//
// 抽 6 次全是同一项,概率上不可能是运气(1/4 的表,6 次全中同一项的几率是 1/4096)。
// 追下去发现 stackable_lottery_pvf.go 里:
//
// roll := int64(0)
// if s.dungeonChoice != nil { ... } // 只在测试里被赋值
//
// —— `dungeonChoice` 在全仓**非测试代码里没有任何一处赋值**,线上它恒为 nil,于是
// roll 永远停在 0,`dungeonDropLotteryPick(entries, 0)` 永远返回第一项。这张卡从来
// 没有真正抽过。
//
// 本用例就是这条的回归锁:**不注入随机源**(线上就是这样)时也必须真的掷骰子。
func TestTarotLotteryRollsWithoutAnInjectedRandomSource(t *testing.T) {
catalog := mustCurrentTarotLotteryTestCatalog(t)
definition, err := catalog.ResolveItem(tarotLotteryTestCardItemID)
if err != nil {
t.Fatal(err)
}
if len(definition.LotteryEntries) != 2 {
t.Fatalf("夹具奖池=%d 项 want 2(这条用例的前提)", len(definition.LotteryEntries))
}
// 刻意**不**设 service.dungeonChoice:这就是线上的形状。
service := &Service{pvfItemCatalog: catalog}
const draws = 200
first, second := definition.LotteryEntries[0].ItemID, definition.LotteryEntries[1].ItemID
seen := map[int64]int{}
sawRollOutsideRange := false
for i := 0; i < draws; i++ {
resolution, resolveErr := service.resolveCurrentTarotLotteryCard(definition)
if resolveErr != nil {
t.Fatalf("第 %d 次抽取失败: %v", i, resolveErr)
}
if resolution == nil || resolution.Drawn == nil {
t.Fatalf("第 %d 次抽取没有结果(missed=%t)", i, resolution != nil && resolution.Missed)
}
if resolution.Roll < 0 || resolution.Roll >= resolution.TotalWeight {
sawRollOutsideRange = true
}
seen[resolution.Drawn.Reward.ItemID]++
}
if sawRollOutsideRange {
t.Fatalf("%d 次抽取里有 roll 落在 [0,total) 之外", draws)
}
// 权重 10000:10000,200 次里两项都该出现。钉死"不止一项"就够 ——
// 旧代码(roll 恒 0)在这里必红:它只可能产出第一项。
if seen[second] == 0 {
t.Fatalf("抽了 %d 次,第二项 %d 一次都没出现(命中分布=%v)——"+
"roll 又被钉死了", draws, second, seen)
}
if seen[first] == 0 {
t.Fatalf("抽了 %d 次,第一项 %d 一次都没出现(命中分布=%v)", draws, first, seen)
}
t.Logf("dungeonChoice=nil(线上形状)下 %d 次抽取分布=%v", draws, seen)
}
// 注入源在时必须仍然优先,这样既有测试与"可复现抽样"的能力不被这次兜底破坏。
func TestTarotLotteryPrefersTheInjectedRandomSource(t *testing.T) {
catalog := mustCurrentTarotLotteryTestCatalog(t)
definition, err := catalog.ResolveItem(tarotLotteryTestCardItemID)
if err != nil {
t.Fatal(err)
}
service := &Service{pvfItemCatalog: catalog}
second := definition.LotteryEntries[1]
boundary := definition.LotteryEntries[0].Weight // 第二项的起始 roll
service.dungeonChoice = func(int) (int, error) { return int(boundary), nil }
resolution, err := service.resolveCurrentTarotLotteryCard(definition)
if err != nil {
t.Fatal(err)
}
if resolution == nil || resolution.Drawn == nil {
t.Fatal("注入源下没有抽到结果")
}
if got := resolution.Drawn.Reward.ItemID; got != second.ItemID {
t.Fatalf("注入 roll=%d 抽到 %d want %d(第二项)", boundary, got, second.ItemID)
}
if resolution.Roll != boundary {
t.Fatalf("resolution.Roll=%d want %d", resolution.Roll, boundary)
}
}
// 注入源越界必须拒绝,而不是被静默夹到边界上:宁可这次不生效,也不能给出错误的奖。
func TestTarotLotteryRejectsAnOutOfRangeInjectedRoll(t *testing.T) {
catalog := mustCurrentTarotLotteryTestCatalog(t)
definition, err := catalog.ResolveItem(tarotLotteryTestCardItemID)
if err != nil {
t.Fatal(err)
}
service := &Service{pvfItemCatalog: catalog}
total := dungeonDropLotteryTotalWeight(definition.LotteryEntries)
if total <= 0 {
t.Fatal("夹具奖池总权重必须为正")
}
service.dungeonChoice = func(int) (int, error) { return int(total), nil } // total 已在区间外
if _, err := service.resolveCurrentTarotLotteryCard(definition); err == nil {
t.Fatal("越界的注入 roll 必须报错,不能被当成合法抽取")
}
}