LotteryService.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. <?php
  2. namespace app\api\library\mlbb;
  3. /**
  4. * 抽奖服务
  5. * 规则:
  6. * 1. 总共12次抽奖机会;四个礼包+5个代金卷,共9个奖品;
  7. * 2. 前四次抽奖,必定包含3个不同的礼包和1个代金卷;
  8. * 3. 礼包在12次抽奖中不可重复获得;
  9. * 4. 抽奖概率:1元代金卷12%,其他所有奖品(包括礼包和其他代金卷)均为11%。
  10. */
  11. class LotteryService
  12. {
  13. /** @var array 原始奖品池 */
  14. private $prizePool = [];
  15. public function __construct(array $prizePool)
  16. {
  17. $this->prizePool = $prizePool;
  18. }
  19. /**
  20. * 单次抽奖
  21. * @param array $userHistory 用户已抽奖历史
  22. * @return array 中奖奖品
  23. */
  24. public function draw(array $userHistory): array
  25. {
  26. $drawCount = count($userHistory) + 1; // 当前是第几抽
  27. $pool = $this->preparePool($userHistory, $drawCount);
  28. $result = $this->weightedDraw($pool);
  29. return $result;
  30. }
  31. /**
  32. * 根据历史和规则准备奖品池
  33. */
  34. private function preparePool(array $userHistory, int $drawCount): array
  35. {
  36. $pool = [];
  37. // 已中过礼包
  38. $gotPacks = array_column(
  39. array_filter($userHistory, function ($h) {
  40. return $h['prize_type'] == 1;
  41. }),
  42. 'prize_mark'
  43. );
  44. foreach ($this->prizePool as $item) {
  45. // 礼包已中过,权重清零
  46. if ($item['type'] === 'pack' && in_array($item['prize_mark'], $gotPacks)) {
  47. $item['weight'] = 0;
  48. }
  49. $pool[$item['prize_mark']] = $item;
  50. }
  51. // 规则:前 4 抽
  52. if ($drawCount <= 4) {
  53. $packCount = count(array_filter($userHistory, function ($h) {
  54. return $h['prize_type'] == 1;
  55. }));
  56. $couponCount = count(array_filter($userHistory, function ($h) {
  57. return $h['prize_type'] == 2;
  58. }));
  59. // 前 3 次必须保证能抽到礼包
  60. if ($packCount < 3 && $drawCount <= 4) {
  61. // 仅保留礼包
  62. $pool = array_filter($pool, function ($p) {
  63. return $p['type'] === 'pack' && $p['weight'] > 0;
  64. });
  65. }
  66. // 第 4 次必须保证出代金券
  67. if ($drawCount == 4 && $couponCount < 1) {
  68. $pool = array_filter($pool, function ($p) {
  69. return $p['type'] === 'coupon' && $p['weight'] > 0;
  70. });
  71. }
  72. }
  73. return $pool;
  74. }
  75. /**
  76. * 加权随机抽取
  77. */
  78. private function weightedDraw(array $pool): array
  79. {
  80. $total = array_sum(array_column($pool, 'weight'));
  81. if ($total <= 0) {
  82. throw new \Exception("奖品池权重总和为 0,无法抽取");
  83. }
  84. $r = mt_rand() / mt_getrandmax() * $total;
  85. $acc = 0.0;
  86. foreach ($pool as $item) {
  87. if ($item['weight'] <= 0) continue;
  88. $acc += $item['weight'];
  89. if ($r <= $acc) {
  90. return $item;
  91. }
  92. }
  93. // 兜底
  94. foreach (array_reverse($pool, true) as $item) {
  95. if ($item['weight'] > 0) return $item;
  96. }
  97. throw new \Exception("未能选出奖品");
  98. }
  99. }
  100. // // 奖品池(使用整数权重)
  101. // $prizePool = [
  102. // ['prize_mark' => 1, 'prize_name' => '殿堂礼包', 'type' => 'pack', 'weight' => 11],
  103. // ['prize_mark' => 2, 'prize_name' => '勇者晋级礼包', 'type' => 'pack', 'weight' => 11],
  104. // ['prize_mark' => 3, 'prize_name' => '勇者冒险礼包', 'type' => 'pack', 'weight' => 11],
  105. // ['prize_mark' => 4, 'prize_name' => '勇者荣耀礼包', 'type' => 'pack', 'weight' => 11],
  106. // ['prize_mark' => 5, 'prize_name' => '1元代金券', 'type' => 'coupon', 'weight' => 12],
  107. // ['prize_mark' => 6, 'prize_name' => '25元代金券', 'type' => 'coupon', 'weight' => 11],
  108. // ['prize_mark' => 7, 'prize_name' => '40元代金券', 'type' => 'coupon', 'weight' => 11],
  109. // ['prize_mark' => 8, 'prize_name' => '65元代金券', 'type' => 'coupon', 'weight' => 11],
  110. // ['prize_mark' => 9, 'prize_name' => '100元代金券','type' => 'coupon', 'weight' => 11],
  111. // ];
  112. //
  113. // $lottery = new LotteryService($prizePool);
  114. //
  115. // $totalRounds = 10000; // 模拟 1 万轮
  116. // $counts = [];
  117. //
  118. // for ($round = 0; $round < $totalRounds; $round++) {
  119. // $userHistory = [];
  120. //
  121. // // 每轮 12 抽
  122. // for ($i = 0; $i < 12; $i++) {
  123. // $result = $lottery->draw($userHistory);
  124. // $mark = $result['prize_mark'];
  125. //
  126. // if (!isset($counts[$mark])) {
  127. // $counts[$mark] = 0;
  128. // }
  129. // $counts[$mark]++;
  130. //
  131. // // 记录历史
  132. // $userHistory[] = [
  133. // 'prize_mark' => $mark,
  134. // 'prize_type' => $result['type'] === 'pack' ? 1 : 2,
  135. // ];
  136. // }
  137. // }
  138. //
  139. // // 输出统计结果
  140. // $totalDraws = $totalRounds * 12;
  141. // echo "=== 模拟 {$totalRounds} 轮(共 {$totalDraws} 次抽奖)结果 ===\n";
  142. // foreach ($prizePool as $p) {
  143. // $mark = $p['prize_mark'];
  144. // $count = $counts[$mark] ?? 0;
  145. // $ratio = $count / $totalDraws * 100;
  146. // echo "{$p['prize_name']} : {$count} 次, 占比 " . number_format($ratio, 2) . "%\n";
  147. // }