Files
bd2/go/internal/server/gacha/point_exchange_test.go
T
Flechazo 5b5a219dc2 feat(server): add special loot and data-driven gameplay rules
Generate equipment loot atomically, grant random boxes and prevent repeat claims.
Read gacha pools, pity, products, tickets, missions, presets, chapter rewards
and inventory costs from GameData; extend both collaboration UR ticket pools.
Sync owned recipes, configure startup state and calendars in versioned seeds,
and validate unlock tickets and revival talents.
2026-10-04 19:15:59 +08:00

200 lines
7.6 KiB
Go

package gacha
import (
"testing"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/player"
"bd2server/internal/server/stateio"
"bd2server/internal/server/wire"
)
func TestGachaPointExchangeGrantsUpgradesOverflowsAndRetries(t *testing.T) {
const (
groupID = uint64(205)
gachaID = uint64(11000145)
rollID = uint64(60901)
pickupID = uint64(21201)
cost = uint64(200)
)
characters := map[uint64]gamedata.CharacterDesign{
rollID: {ID: 6090, HP: 253, CostumeMaxLevel: 100, OverflowItemType: 20, OverflowItemCount: 2},
pickupID: {ID: 2120, HP: 300, CostumeMaxLevel: 5, OverflowItemType: 20, OverflowItemCount: 2},
}
infinite, err := fixtureInfiniteGachaDesign(10, []uint64{rollID}, characters)
if err != nil {
t.Fatal(err)
}
regular, err := fixtureRegularCatalog(map[uint64]gamedata.RegularGacha{
gachaID: {ID: gachaID, Count: 10, PriceType: 3, Price: 2000, Pool: []gamedata.WeightedCostume{{ID: rollID, Weight: 1}}},
}, characters)
if err != nil {
t.Fatal(err)
}
group := gamedata.GachaGroupDesign{
ID: groupID, GachaType: 1, PointCount: 140,
PickUpExchangeCost: cost, PickUpCostumeID: pickupID, TenTimeGachaID: gachaID,
}
if err := regular.AddGroupDesign(group, gamedata.GachaFixedDesign{}); err != nil {
t.Fatal(err)
}
storage := stateio.NewMemory()
collection, err := player.OpenCollectionStore(storage, nil)
if err != nil {
t.Fatal(err)
}
seed := make([]uint64, 10)
for i := range seed {
seed[i] = rollID
}
if _, err := collection.GrantRegularPurchase("point-seed", seed, regular, player.GachaPurchase{Group: group, BuyType: 1}); err != nil {
t.Fatal(err)
}
wallet, err := player.OpenWallet(storage, player.Currency{})
if err != nil {
t.Fatal(err)
}
service, err := NewService(infinite, regular, collection, wallet)
if err != nil {
t.Fatal(err)
}
service.BeginSession("point-costume-login")
var lastRequest, lastResponse []byte
for exchange := uint64(0); exchange < 7; exchange++ {
request := wire.AppendVarint(nil, 1, 100+exchange)
request = wire.AppendVarint(request, 2, groupID)
code, response, handled, err := service.Handle("/GachaPointExchange", request)
if err != nil || !handled || code != 147 {
t.Fatalf("exchange %d code=%d handled=%v err=%v", exchange, code, handled, err)
}
bundle, found, err := wire.Bytes(response, 1)
if err != nil || !found {
t.Fatalf("exchange %d reward bundle found=%v err=%v", exchange, found, err)
}
switch {
case exchange == 0:
if countFields(bundle, 2) != 1 || countFields(bundle, 3) != 1 || countFields(bundle, 9) != 0 {
t.Fatalf("new pickup reward characters=%d costumes=%d upgrades=%d bundle=%x", countFields(bundle, 2), countFields(bundle, 3), countFields(bundle, 9), bundle)
}
case exchange < 6:
if countFields(bundle, 3) != 0 || countFields(bundle, 9) != 1 || countFields(bundle, 8) != 0 {
t.Fatalf("upgrade %d costumes=%d upgrades=%d exchanges=%d bundle=%x", exchange, countFields(bundle, 3), countFields(bundle, 9), countFields(bundle, 8), bundle)
}
default:
if countFields(bundle, 9) != 1 || countFields(bundle, 8) != 1 || countFields(bundle, 10) != 1 {
t.Fatalf("overflow upgrades=%d exchanges=%d repaid=%d bundle=%x", countFields(bundle, 9), countFields(bundle, 8), countFields(bundle, 10), bundle)
}
}
lastRequest, lastResponse = request, response
}
if user := collection.GachaUser(groupID); user.Point != 0 || user.ExchangeItemCount != 7 || user.ExchangeMileageCount != 0 {
t.Fatalf("gacha user after exchanges=%+v", user)
}
if got := wallet.Snapshot().Mileage; got != 2 {
t.Fatalf("overflow mileage=%d want=2", got)
}
owned := collection.Costumes()
if len(owned) != 2 || owned[1].ID != pickupID || owned[1].Level != 5 {
t.Fatalf("owned costumes=%+v", owned)
}
// A byte-identical transport retry must recover the persisted grant and
// wallet credit without another debit, level change, or exchange count.
code, replay, handled, err := service.Handle("/GachaPointExchange", lastRequest)
if err != nil || !handled || code != 147 || string(replay) != string(lastResponse) {
t.Fatalf("retry code=%d handled=%v err=%v response=%x want=%x", code, handled, err, replay, lastResponse)
}
if user := collection.GachaUser(groupID); user.Point != 0 || user.ExchangeItemCount != 7 {
t.Fatalf("retry changed gacha user=%+v", user)
}
if got := wallet.Snapshot().Mileage; got != 2 {
t.Fatalf("retry changed mileage=%d", got)
}
restored, err := player.OpenCollectionStore(storage, nil)
if err != nil {
t.Fatal(err)
}
restarted, err := NewService(infinite, regular, restored, wallet)
if err != nil {
t.Fatal(err)
}
restarted.BeginSession("point-costume-login")
_, replay, _, err = restarted.Handle("/GachaPointExchange", lastRequest)
if err != nil || string(replay) != string(lastResponse) {
t.Fatalf("restart retry err=%v response=%x want=%x", err, replay, lastResponse)
}
if user := restored.GachaUser(groupID); user.Point != 0 || user.ExchangeItemCount != 7 {
t.Fatalf("restored gacha user=%+v", user)
}
}
func TestGachaPointExchangeUsesOnlySavedSingleSelection(t *testing.T) {
const (
groupID = uint64(1001)
gachaID = uint64(11000001)
rollID = uint64(60901)
selected = uint64(64901)
)
characters := map[uint64]gamedata.CharacterDesign{
rollID: fixtureCharacter(6090, 253),
selected: fixtureCharacter(6490, 300),
}
infinite, err := fixtureInfiniteGachaDesign(10, []uint64{rollID}, characters)
if err != nil {
t.Fatal(err)
}
regular, err := fixtureRegularCatalog(map[uint64]gamedata.RegularGacha{
gachaID: {ID: gachaID, Count: 1, PriceType: 3, Price: 200, Pool: []gamedata.WeightedCostume{{ID: rollID, Weight: 1}}},
}, characters)
if err != nil {
t.Fatal(err)
}
group := gamedata.GachaGroupDesign{ID: groupID, GachaType: 1, PointCount: 200, PickUpExchangeCost: 200, OneTimeGachaID: gachaID, SelectCount: 1}
if err := regular.AddGroupDesign(group, gamedata.GachaFixedDesign{}); err != nil {
t.Fatal(err)
}
storage := stateio.NewMemory()
collection, err := player.OpenCollectionStore(storage, nil)
if err != nil {
t.Fatal(err)
}
if _, err := collection.GrantRegularPurchase("selection-point-seed", []uint64{rollID}, regular, player.GachaPurchase{Group: group, BuyType: 1}); err != nil {
t.Fatal(err)
}
if err := collection.SaveGachaSelections(groupID, []player.GachaSelection{{GroupID: groupID, Slot: 1, ItemID: selected}}, 0); err != nil {
t.Fatal(err)
}
wallet, err := player.OpenWallet(storage, player.Currency{})
if err != nil {
t.Fatal(err)
}
service, err := NewService(infinite, regular, collection, wallet)
if err != nil {
t.Fatal(err)
}
service.BeginSession("selection-point-login")
bad := wire.AppendVarint(wire.AppendVarint(wire.AppendVarint(nil, 1, 1), 2, groupID), 3, rollID)
if code, _, handled, err := service.Handle("/GachaPointExchange", bad); err == nil || !handled || code != 147 {
t.Fatalf("unsaved selection code=%d handled=%v err=%v", code, handled, err)
}
if user := collection.GachaUser(groupID); user.Point != 200 || user.ExchangeItemCount != 0 {
t.Fatalf("rejected selection changed user=%+v", user)
}
request := wire.AppendVarint(wire.AppendVarint(wire.AppendVarint(nil, 1, 2), 2, groupID), 3, selected)
code, response, handled, err := service.Handle("/GachaPointExchange", request)
if err != nil || !handled || code != 147 {
t.Fatalf("saved selection code=%d handled=%v err=%v", code, handled, err)
}
bundle, found, err := wire.Bytes(response, 1)
if err != nil || !found || countFields(bundle, 3) != 1 {
t.Fatalf("saved selection reward found=%v costumes=%d err=%v bundle=%x", found, countFields(bundle, 3), err, bundle)
}
if user := collection.GachaUser(groupID); user.Point != 0 || user.ExchangeItemCount != 1 {
t.Fatalf("saved selection user=%+v", user)
}
}