Files
90CNServer/server/internal/modules/dnf/pvp/rating_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

117 lines
5.1 KiB
Go

package pvp
import (
"context"
"math"
"reflect"
"testing"
"time"
repo "longheng.io/server/internal/modules/dnf/repository"
"longheng.io/server/internal/modules/dnf/repository/memory"
)
func TestPVPRatingSettlementRulesAndDurability(t *testing.T) {
c := testCatalog(t)
for _, tc := range []struct {
name string
exp, grade, rp, played, wins, losses, opponent int64
won, draw, manual, rated bool
wantEXP, wantGrade, wantRP int64
}{
{name: "seed win", won: true, wantEXP: 1000, wantRP: 1050},
{name: "seed loss", wantRP: 950},
{name: "grade up", exp: 11950, rp: 1000, played: 11, opponent: 1000, won: true, wantEXP: 12950, wantGrade: 1, wantRP: 1011},
{name: "grade down signed loss", exp: 12010, grade: 1, rp: 1000, played: 11, opponent: 1000, wantEXP: 11796, wantRP: 989},
{name: "warmup completion", rp: 1000, played: 9, opponent: 1000, won: true, wantEXP: 1000, wantRP: 1050},
{name: "first post warmup bonus", rp: 1000, played: 10, opponent: 1000, won: true, wantEXP: 1000, wantRP: 1051},
{name: "four win multiplier", rp: 1000, played: 11, wins: 3, opponent: 1000, won: true, wantEXP: 1000, wantRP: 1013},
{name: "two loss multiplier", rp: 1000, played: 11, losses: 1, opponent: 1000, wantRP: 987},
{name: "higher rating draw", rp: 1500, played: 11, opponent: 1000, draw: true, wantRP: 1494},
{name: "manual win", rp: 1234, manual: true, won: true, wantEXP: 450, wantRP: 1234},
{name: "manual loss", exp: 12500, grade: 1, rp: 1234, manual: true, wantEXP: 12307, wantGrade: 1, wantRP: 1234},
{name: "zero floor stays initialized", rp: 0, played: 11, opponent: 1000, rated: true, wantRP: 0},
{name: "exp cap", exp: 26990, grade: 1, rp: 1000, played: 11, opponent: 1000, won: true, wantEXP: 27000, wantGrade: 1, wantRP: 1011},
} {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
repos := memory.NewMemoryGroup()
now := time.Date(2026, 9, 6, 9, 0, 0, 0, time.UTC)
stats := map[string]int64{ExperienceKey: tc.exp, GradeKey: tc.grade, RankPointKey: tc.rp, MaxRankKey: tc.rp,
CertificationKey: tc.played, "pvp_win_streak": tc.wins, "pvp_loss_streak": tc.losses, "exp": 76543}
if tc.rated {
stats["pvp_rating_initialized"] = 1
}
if err := repos.Character.Save(ctx, repo.CharacterRecord{CharacterID: "1", AccountID: "a", Stats: stats}); err != nil {
t.Fatal(err)
}
before, err := Prepare(ctx, repos, c, "a", "1", now)
if err != nil {
t.Fatal(err)
}
match := Match{Key: tc.name, StartedAt: now, CompletedAt: now.Add(time.Minute), Won: tc.won, Draw: tc.draw,
Manual: tc.manual, OpponentRP: tc.opponent}
failed := repos
failed.CharacterSettlement = failCommit{repos.CharacterSettlement}
if _, err := Complete(ctx, failed, c, "a", "1", match); err == nil {
t.Fatal("commit failure ignored")
}
unchanged, _, _ := repos.Character.Load(ctx, "1")
if !reflect.DeepEqual(unchanged.Stats, before.Character.Stats) {
t.Fatal("failed transaction changed rating/experience")
}
for retry := 0; retry < 2; retry++ {
after, err := Complete(ctx, repos, c, "a", "1", match)
if err != nil {
t.Fatal(err)
}
s := after.Character.Stats
if s[ExperienceKey] != tc.wantEXP || s[GradeKey] != tc.wantGrade || s[RankPointKey] != tc.wantRP || after.ExperienceDelta != tc.wantEXP-tc.exp || after.BeforeGrade == nil || *after.BeforeGrade != uint32(tc.grade) {
t.Fatalf("retry=%d exp=%d grade=%d rp=%d delta=%d", retry, s[ExperienceKey], s[GradeKey], s[RankPointKey], after.ExperienceDelta)
}
if s["exp"] != 76543 || s[MaxRankKey] != max(tc.rp, tc.wantRP) {
t.Fatal("normal exp or highest RP corrupted")
}
wantPlayed := tc.played + 1
if tc.manual {
wantPlayed = tc.played
}
if s[CertificationKey] != wantPlayed {
t.Fatal("mode or receipt replay altered certification")
}
if tc.manual && (!reflect.DeepEqual(after.Rows, before.Rows) || s[WinsKey] != 0 || s[LossesKey] != 0 || s["pvp_rating_initialized"] != before.Character.Stats["pvp_rating_initialized"]) {
t.Fatal("manual result mutated ranked facts")
}
}
restored, err := Prepare(ctx, repos, c, "a", "1", now.Add(24*time.Hour))
if err != nil || restored.Character.Stats[RankPointKey] != tc.wantRP || restored.Character.Stats[ExperienceKey] != tc.wantEXP {
t.Fatal("day reset/reconnect lost rating")
}
})
}
for _, key := range []string{ExperienceKey, GradeKey, RankPointKey, MaxRankKey, "pvp_win_streak"} {
stats := map[string]int64{key: math.MaxInt32 + 1}
if _, err := c.applyRating(stats, Match{}, 0); err == nil {
t.Fatalf("invalid %s accepted", key)
}
}
}
func TestPVFRatingRulesRejectMissingAndMalformedTables(t *testing.T) {
for _, replacement := range []string{
"[pvp grade point]\n0 1000\n[/pvp grade point]",
"[pvp experience]\n1 12000 2 27000\n[/pvp experience]",
} {
s := dailyTestSource()
s["etc/pvp_ref.etc"] = replacement
if _, err := Load(s); err == nil {
t.Fatal("incomplete grade/mode data accepted")
}
}
s := dailyTestSource()
delete(s, "etc/matchingmode.etc")
if _, err := Load(s); err == nil {
t.Fatal("missing rating configuration silently substituted")
}
}