java 抽奖程序,自定义抽奖概率和奖品,模拟王者荣耀水晶抽奖

一、定义奖品实体类

@Data
@Builder
public class LuckDrawRules {
    /**
     * id
     */
    private Long id;

    /**
     * 奖品名称
     */
    private String title;

    /**
     * 中奖概率
     */
    private Double rate;

    /**
     * 奖品图片
     */
    private JSONArray image;
}

二、实现类

public interface LuckDrawService {

    /**
     * 抽奖
     * @return
     */
    LuckDrawRules luckDraw();
}
@Service
public class LuckDrawServiceImpl implements LuckDrawService {

    // 构造奖品列表数据,自定义中奖概率,可根据业务将奖品添加到数据库
    private List<LuckDrawRules> buildRule() {
        List<LuckDrawRules> rules = new ArrayList<>();
        rules.add(LuckDrawRules.builder().id(1L).rate(0.022).title("永久英雄*李白").build());
        rules.add(LuckDrawRules.builder().id(2L).rate(0.317).title("皮肤碎片*5").build());
        rules.add(LuckDrawRules.builder().id(3L).rate(0.355).title("铭文碎片*10").build());
        rules.add(LuckDrawRules.builder().id(4L).rate(0.159).title("钻石*10").build());
        rules.add(LuckDrawRules.builder().id(5L).rate(0.139).title("金币*200").build());
        rules.add(LuckDrawRules.builder().id(6L).rate(0.008).title("荣耀水晶").build());
        rules.add(LuckDrawRules.builder().id(7L).rate(0.011).title("永久皮肤*死神来了").build());
        rules.add(LuckDrawRules.builder().id(8L).rate(0.011).title("永久皮肤*冰冠公主").build());
        rules.add(LuckDrawRules.builder().id(9L).rate(0.5).title("亲密玫瑰*5").build());
        rules.add(LuckDrawRules.builder().id(10L).rate(0.5).title("战令币*50").build());
        return rules;
    }

    /**
     * 抽奖
     * @return
     */
    @Override
    public LuckDrawRules luckDraw() {
        List<LuckDrawRules> luckDrawRules = buildRule();
        List<Double> originalRates = new ArrayList<>(luckDrawRules.size());
        for (LuckDrawRules rule : luckDrawRules) {
            double probability = rule.getRate() < 0 ? 0 : rule.getRate();
            originalRates.add(probability);
        }
        int originalIndex = lottery(originalRates);
        return luckDrawRules.get(originalIndex);
    }

    private int lottery(List<Double> originalRates) {
        if (CollUtil.isEmpty(originalRates)) {
            return -1;
        }
        // 计算总概率,这样可以保证不一定总概率是1
        double sumRate = 0d;
        for (double rate : originalRates) {
            sumRate += rate;
        }

        // 计算每个物品在总概率的基础下的概率情况
        List<Double> sortOriginalRates = new ArrayList<>(originalRates.size());
        double tempSumRate = 0d;
        for (double rate : originalRates) {
            tempSumRate += rate;
            sortOriginalRates.add(tempSumRate / sumRate);
        }

        // 根据区块值来获取抽取到的物品索引
        double nextDouble = Math.random();
        sortOriginalRates.add(nextDouble);
        Collections.sort(sortOriginalRates);
        return sortOriginalRates.indexOf(nextDouble);
    }
}

三、Controller

可根据参数count来进行单抽或五连抽

抽到重复奖品不去重,如下:

@RestController
public class LuckDrawController {

    @Resource
    private LuckDrawService luckDrawService;

    /**
     * 不去重
     * @param count 抽奖次数
     * @return
     */
    @GetMapping("/luckDraw")
    public List<LuckDrawRules> luckDraw(Integer count) {

        List<LuckDrawRules> list = new ArrayList<>();
        for (int i = 0; i < count ; i++) {
            LuckDrawRules luckDraw = luckDrawService.luckDraw();
            list.add(luckDraw);
        }
        return list;
    }
}

结果:

 如果需要奖品重复,数量叠加的话,如下:

@RestController
public class LuckDrawController {

    @Resource
    private LuckDrawService luckDrawService;

    /**
     * 去重,计算重复的数量
     * @param count 抽奖次数
     * @return
     */
    @GetMapping("/luckDrawDuplicate")
    public Map<String, Long> luckDrawDuplicate(Integer count) {
        List<LuckDrawRules> list = new ArrayList<>();
        for (int i = 0; i < count ; i++) {
            LuckDrawRules luckDraw = luckDrawService.luckDraw();
            list.add(luckDraw);
        }
        //查询重复出现的元素及个数
        Map<String, Long> map = list.stream().collect(Collectors.groupingBy(LuckDrawRules::getTitle, Collectors.counting()));
        // 根据value排序
        return map.entrySet().stream()
                // 根据键排序 comparingByKey()
                // 根据中奖次数排序
                .sorted(Map.Entry.comparingByValue())
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        Map.Entry::getValue,
                        (oldVal, newVal) -> oldVal,
                        LinkedHashMap::new
                ));
    }
}

结果:

以上抽奖核心代码的基本原理如下:

  1. 根据输入的中奖概率建立奖励规则列表
  2. 计算每个奖品对应的中奖概率和总概率
  3. 将总概率转换为[0,1]区间内的概率值
  4. 生成一个随机数,映射到[0,1]区间内
  5. 根据随机数落入的区间对应到对应的奖品得主

可以发现,这个抽奖算法的核心是将中奖概率映射到[0,1]区间上,并在该区间内产生随机数。通过将总概率分段映射到[0,1]上,在[0,1]上生成的随机数可以直接对应到对应的奖品中,从而确定获奖者。此外,还考虑了输入数据的异常情况(例如中奖概率小于0),也代码中进行了处理。

详细图解:

当概率给定为P1、P2、P3时,把概率映射到[0,1]区间上的过程可以表示为:

 |<--- P1 --->|<--- P2 --->|<--- P3 --->|

   0                  x             y            1

其中,x = P1 / (P1+P2+P3),y = (P1+P2) / (P1+P2+P3)。

生成一个在 [0,1] 区间内的随机数,如下图所示:

 |<--- P1 --->|<--- P2 --->|<--- P3 --->|

0                 R              y            1

根据随机数R所在的区间可以得到相应的奖品编号。

注意,这里的 sortOriginalRates 实际上是每个奖品的累积概率,因此将它与随机数 R 进行比较来确定落入哪个区间,并返回对应的奖励规则。

如果还不理解,那再进一步解释一下:

这段代码的主要思路是:根据每个奖品获奖概率的大小,把[0,1]区间分成多个小区间,并把它们的长度与每个奖品的概率对应起来。然后,生成一个0~1之间的随机数r,根据r所对应的小区间的编号,得到获奖的奖品。

假设有三个奖品,他们的中奖概率分别为P1, P2, P3,其和为S=P1+P2+P3。我们将[0,1]区间等分成n份,每个奖品获奖的区间长度就是它的中奖概率除以总概率S,并将它们相加,得到对应的序列sortOriginalRates。这个序列的最后一位是1,用来处理随机数r刚好落在最后一个区间的特殊情况。

接着,我们生成一个0~1之间的随机数R。我们将R插入到sortOriginalRates中,排序后,R所在的位置即为获奖的奖品对应的序号。

这种方法的好处是不需要事先对奖品的概率进行归一化,可以直接使用原始值进行计算。同时,计算简单、速度快,适合处理中小规模的抽奖活动。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值