207 lines
6.1 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 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
})
}