207 lines
6.1 KiB
Go
207 lines
6.1 KiB
Go
package main
|
||
|
||
import (
|
||
"crypto/hmac"
|
||
"crypto/sha256"
|
||
"encoding/binary"
|
||
"encoding/hex"
|
||
"errors"
|
||
"fmt"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// RewardItem 简化的奖品结构
|
||
type RewardItem struct {
|
||
ID int64
|
||
Name string
|
||
Weight int
|
||
Count int // for ichiban expansion
|
||
}
|
||
|
||
// VerifyUnlimited 验证无限赏结果
|
||
func VerifyUnlimited(seedHex string, issueID int64, userID int64, saltHex string, rewards []RewardItem) (int64, string, error) {
|
||
// 1. Decode inputs
|
||
seedKey, err := decodeHex(seedHex)
|
||
if err != nil {
|
||
return 0, "", fmt.Errorf("invalid seed: %v", err)
|
||
}
|
||
salt, err := decodeHex(saltHex)
|
||
if err != nil {
|
||
return 0, "", fmt.Errorf("invalid salt: %v", err)
|
||
}
|
||
|
||
// Sort rewards by ID to ensure consistency with backend (which usually iterates by ID)
|
||
// This fixes issues where input JSON is not sorted.
|
||
// Note: For Ichiban, sorting might be more complex (see VerifyIchiban comments).
|
||
// But for Unlimited, ID sort is the confirmed behavior.
|
||
sortRewardsByID(rewards)
|
||
|
||
// 2. Calculate Total Weight
|
||
var totalWeight int64
|
||
for _, r := range rewards {
|
||
totalWeight += int64(r.Weight)
|
||
}
|
||
if totalWeight <= 0 {
|
||
return 0, "", errors.New("total weight must be > 0")
|
||
}
|
||
|
||
// 3. HMAC Logic (Same as strategy/default.go)
|
||
mac := hmac.New(sha256.New, seedKey)
|
||
mac.Write([]byte(fmt.Sprintf("draw:issue:%d|user:%d|salt:%x", issueID, userID, salt)))
|
||
sum := mac.Sum(nil)
|
||
rnd := int64(binary.BigEndian.Uint64(sum[:8]) % uint64(totalWeight))
|
||
|
||
// 4. Select Reward
|
||
var acc int64
|
||
var pickedID int64
|
||
var pickedName string
|
||
|
||
processLog := fmt.Sprintf("Total Weight: %d\nRandom Value: %d\n", totalWeight, rnd)
|
||
|
||
for _, r := range rewards {
|
||
acc += int64(r.Weight)
|
||
if rnd < acc {
|
||
pickedID = r.ID
|
||
pickedName = r.Name
|
||
processLog += fmt.Sprintf("WIN: [%d] %s (Range: %d-%d)\n", r.ID, r.Name, acc-int64(r.Weight), acc)
|
||
processLog += fmt.Sprintf("Selected Item Name: %s\n", pickedName)
|
||
break
|
||
}
|
||
}
|
||
|
||
return pickedID, processLog, nil
|
||
}
|
||
|
||
// VerifyIchiban 验证一番赏结果
|
||
func VerifyIchiban(seedHex string, issueID int64, slotIndex int, rewardsInput []RewardItem) (int64, string, error) {
|
||
// 1. Expand Rewards to Slots
|
||
// 一番赏逻辑:将 A:2, B:3 展开为 [A, A, B, B, B]
|
||
// 且排序必须确定(ID升序 或 输入顺序? Backend is: IsBoss Desc, Level Asc, Sort Asc, ID Asc.
|
||
// 验证工具这里简单起见,假设输入已经是有序的配置列表
|
||
|
||
var slots []RewardItem
|
||
for _, r := range rewardsInput {
|
||
for k := 0; k < r.Count; k++ {
|
||
slots = append(slots, r)
|
||
}
|
||
}
|
||
|
||
totalSlots := len(slots)
|
||
if totalSlots == 0 {
|
||
return 0, "", errors.New("no slots generated from rewards")
|
||
}
|
||
|
||
if slotIndex < 1 || slotIndex > totalSlots {
|
||
return 0, "", fmt.Errorf("slot index %d out of range [1, %d]", slotIndex, totalSlots)
|
||
}
|
||
|
||
// 2. Decode Seed
|
||
seedKey, err := decodeHex(seedHex)
|
||
if err != nil {
|
||
return 0, "", fmt.Errorf("invalid seed: %v", err)
|
||
}
|
||
|
||
// 3. Shuffle (Same as strategy/ichiban.go)
|
||
// Create a mapping index array to shuffle
|
||
indices := make([]int, totalSlots)
|
||
for i := 0; i < totalSlots; i++ {
|
||
indices[i] = i
|
||
}
|
||
|
||
mac := hmac.New(sha256.New, seedKey)
|
||
for i := totalSlots - 1; i > 0; i-- {
|
||
mac.Reset()
|
||
mac.Write([]byte(fmt.Sprintf("shuffle:%d|issue:%d", i, issueID)))
|
||
sum := mac.Sum(nil)
|
||
// j := rnd % (i+1)
|
||
j := int(binary.BigEndian.Uint64(sum[:8]) % uint64(i+1))
|
||
indices[i], indices[j] = indices[j], indices[i]
|
||
}
|
||
|
||
// 4. Get Result
|
||
// slotIndex is 1-based from user input
|
||
// mapped index is indices[slotIndex-1]
|
||
// BUT wait, backend: `picked = slots[slotIndex]` (after shuffle logic applied to `slots`)
|
||
// In the backend implementation:
|
||
/*
|
||
slots := ... // filled
|
||
for i:=... {
|
||
swap(slots[i], slots[j])
|
||
}
|
||
picked := slots[slotIndex] // slotIndex passed from req (0-based inside function usually? Let's check backend)
|
||
*/
|
||
// Checked backend: `req.SlotIndex` comes from frontend.
|
||
// `validateIchibanSlots` checks `si < 1`. Code uses `selectedSlots = append(..., si-1)`.
|
||
// `SelectItemBySlot` uses `slotIndex` arg.
|
||
// So `slotIndex` in `SelectItemBySlot` is 0-based.
|
||
// Shuffle operates on `slots`. Finally returns `slots[slotIndex]`.
|
||
// This means "Slot N" (user chosen) always points to PHYSICAL position N.
|
||
// The CONTENT of position N changes after shuffle.
|
||
// OK, my implementation below uses `indices` to track movements, let's align.
|
||
|
||
// Re-implement exactly as backend: shuffle the actual slice
|
||
workingSlots := make([]RewardItem, totalSlots)
|
||
copy(workingSlots, slots)
|
||
|
||
for i := totalSlots - 1; i > 0; i-- {
|
||
mac.Reset()
|
||
mac.Write([]byte(fmt.Sprintf("shuffle:%d|issue:%d", i, issueID)))
|
||
sum := mac.Sum(nil)
|
||
j := int(binary.BigEndian.Uint64(sum[:8]) % uint64(i+1))
|
||
workingSlots[i], workingSlots[j] = workingSlots[j], workingSlots[i]
|
||
}
|
||
|
||
finalReward := workingSlots[slotIndex-1] // 1-based input -> 0-based index
|
||
|
||
log := fmt.Sprintf("Total Slots: %d\nUser Selected Slot: %d\nResult Reward: [%d] %s\n", totalSlots, slotIndex, finalReward.ID, finalReward.Name)
|
||
return finalReward.ID, log, nil
|
||
}
|
||
|
||
// Helpers
|
||
|
||
func decodeHex(s string) ([]byte, error) {
|
||
if len(s)%2 != 0 {
|
||
return nil, errors.New("hex string length must be even")
|
||
}
|
||
// naive hex decode or use pkg
|
||
// here reusing simple implementation or adding imports
|
||
return parseHex(s)
|
||
}
|
||
|
||
func parseHex(s string) ([]byte, error) {
|
||
return hex.DecodeString(s)
|
||
}
|
||
|
||
// ParseRewardsString parses "ID:Weight,ID:Weight" or "ID:Name:Weight"
|
||
func ParseRewardsString(s string) ([]RewardItem, error) {
|
||
parts := strings.Split(s, ",")
|
||
var res []RewardItem
|
||
for _, p := range parts {
|
||
p = strings.TrimSpace(p)
|
||
if p == "" {
|
||
continue
|
||
}
|
||
// Format: ID:Weight or ID:Name:Weight
|
||
sub := strings.Split(p, ":")
|
||
if len(sub) == 2 {
|
||
id, _ := strconv.ParseInt(sub[0], 10, 64)
|
||
w, _ := strconv.Atoi(sub[1])
|
||
res = append(res, RewardItem{ID: id, Name: fmt.Sprintf("Item-%d", id), Weight: w, Count: w})
|
||
} else if len(sub) == 3 {
|
||
id, _ := strconv.ParseInt(sub[0], 10, 64)
|
||
name := sub[1]
|
||
w, _ := strconv.Atoi(sub[2])
|
||
res = append(res, RewardItem{ID: id, Name: name, Weight: w, Count: w})
|
||
}
|
||
}
|
||
return res, nil
|
||
}
|
||
|
||
func sortRewardsByID(rewards []RewardItem) {
|
||
sort.Slice(rewards, func(i, j int) bool {
|
||
return rewards[i].ID < rewards[j].ID
|
||
})
|
||
}
|