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 }) }