diff --git a/services/room-service/internal/room/service/room_rocket.go b/services/room-service/internal/room/service/room_rocket.go index 79c48aeb..f2851457 100644 --- a/services/room-service/internal/room/service/room_rocket.go +++ b/services/room-service/internal/room/service/room_rocket.go @@ -1053,13 +1053,6 @@ func (s *Service) settleRoomRocketRewards(ctx context.Context, now time.Time, cu }) return nil } - addRandomReward := func(role string, userID int64, pool []RoomRocketRewardItem) error { - item, ok := selectRoomRocketReward(launch.RocketID, role, userID, pool) - if !ok { - return nil - } - return addRewardItem(role, userID, item) - } addAllRewards := func(role string, userID int64, pool []RoomRocketRewardItem) error { for _, item := range pool { if err := addRewardItem(role, userID, item); err != nil { @@ -1069,9 +1062,14 @@ func (s *Service) settleRoomRocketRewards(ctx context.Context, now time.Time, cu return nil } + inRoomRewards := selectRoomRocketRewardsForUsers(launch.RocketID, roomRocketRewardInRoom, inRoomUserIDs, levelCfg.InRoomRewards) for _, userID := range inRoomUserIDs { - // 在房奖励是大面积发放池,每个在线用户只按权重抽一个奖项,避免房间人数放大单次火箭成本。 - if err := addRandomReward(roomRocketRewardInRoom, userID, levelCfg.InRoomRewards); err != nil { + // 在房奖励是大面积发放池,每个在线用户只拿一个奖项;同一枚火箭内按无放回周期分配,避免配置了金币和麦位声波时整房都落到同一个奖项。 + item, ok := inRoomRewards[userID] + if !ok { + continue + } + if err := addRewardItem(roomRocketRewardInRoom, userID, item); err != nil { return nil, err } } @@ -1111,7 +1109,56 @@ func (s *Service) grantRoomRocketReward(ctx context.Context, _ time.Time, curren return resp.GetGrant().GetGrantId(), nil } +func validRoomRocketRewardPool(pool []RoomRocketRewardItem) []RoomRocketRewardItem { + valid := make([]RoomRocketRewardItem, 0, len(pool)) + for _, item := range pool { + if item.Weight <= 0 || item.ResourceGroupID <= 0 { + continue + } + valid = append(valid, item) + } + return valid +} + +func selectRoomRocketRewardsForUsers(rocketID string, role string, userIDs []int64, pool []RoomRocketRewardItem) map[int64]RoomRocketRewardItem { + assignments := make(map[int64]RoomRocketRewardItem, len(userIDs)) + if len(userIDs) == 0 { + return assignments + } + basePool := validRoomRocketRewardPool(pool) + if len(basePool) == 0 { + return assignments + } + available := append([]RoomRocketRewardItem(nil), basePool...) + cycle := 0 + for index, userID := range userIDs { + if userID <= 0 { + continue + } + if len(available) == 0 { + // 一轮内每个配置项最多发一次;房间人数超过奖池大小时再开启下一轮,让大房间仍可继续按配置重复分配。 + available = append([]RoomRocketRewardItem(nil), basePool...) + cycle++ + } + selectedIndex, ok := selectRoomRocketRewardIndex(rocketID, role, userID, available, fmt.Sprintf("cycle:%d:index:%d", cycle, index)) + if !ok { + continue + } + assignments[userID] = available[selectedIndex] + available = append(available[:selectedIndex], available[selectedIndex+1:]...) + } + return assignments +} + func selectRoomRocketReward(rocketID string, role string, userID int64, pool []RoomRocketRewardItem) (RoomRocketRewardItem, bool) { + index, ok := selectRoomRocketRewardIndex(rocketID, role, userID, pool, "") + if !ok { + return RoomRocketRewardItem{}, false + } + return pool[index], true +} + +func selectRoomRocketRewardIndex(rocketID string, role string, userID int64, pool []RoomRocketRewardItem, salt string) (int, bool) { total := int64(0) for _, item := range pool { if item.Weight > 0 && item.ResourceGroupID > 0 { @@ -1119,21 +1166,26 @@ func selectRoomRocketReward(rocketID string, role string, userID int64, pool []R } } if total <= 0 { - return RoomRocketRewardItem{}, false + return -1, false } - hash := sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%d", rocketID, role, userID))) + hash := sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%d|%s", rocketID, role, userID, salt))) pick := int64(binary.BigEndian.Uint64(hash[:8]) % uint64(total)) cursor := int64(0) - for _, item := range pool { + fallback := -1 + for index, item := range pool { if item.Weight <= 0 || item.ResourceGroupID <= 0 { continue } + fallback = index cursor += item.Weight if pick < cursor { - return item, true + return index, true } } - return pool[len(pool)-1], true + if fallback >= 0 { + return fallback, true + } + return -1, false } func roomRocketGrantCommandID(rocketID string, role string, userID int64, rewardItemID string) string { diff --git a/services/room-service/internal/room/service/room_rocket_test.go b/services/room-service/internal/room/service/room_rocket_test.go index bbdaaa65..ce26cbde 100644 --- a/services/room-service/internal/room/service/room_rocket_test.go +++ b/services/room-service/internal/room/service/room_rocket_test.go @@ -566,6 +566,9 @@ func TestRoomRocketLaunchStacksAllSpecialPoolsAndRandomInRoomReward(t *testing.T if countRocketRewards(rewards, igniterID, "in_room") != 1 { t.Fatalf("top1 igniter must still receive exactly one random in-room reward: %+v", rewards) } + if countRocketRewardsByRoleAndGroup(rewards, "in_room", 1001) != 1 || countRocketRewardsByRoleAndGroup(rewards, "in_room", 1101) != 1 { + t.Fatalf("in-room rewards must spread across the configured pool before repeating an item: %+v", rewards) + } if countRocketRewards(rewards, igniterID, "top1") != 2 || countRocketRewards(rewards, igniterID, "igniter") != 2 { t.Fatalf("top1 igniter must receive every configured special reward: %+v", rewards) } @@ -828,6 +831,16 @@ func countRocketRewards(rewards []*roomv1.RoomRocketRewardGrant, userID int64, r return count } +func countRocketRewardsByRoleAndGroup(rewards []*roomv1.RoomRocketRewardGrant, role string, groupID int64) int { + count := 0 + for _, reward := range rewards { + if reward.GetRewardRole() == role && reward.GetResourceGroupId() == groupID { + count++ + } + } + return count +} + func rocketRewardUserIDs(rewards []*roomv1.RoomRocketRewardGrant) []int64 { userIDs := make([]int64, 0, len(rewards)) for _, reward := range rewards {