重新优化中奖权重计算方式

This commit is contained in:
2026-03-11 15:40:15 +08:00
parent bb166350fd
commit 2af7fedcce
13 changed files with 330 additions and 322 deletions

View File

@@ -36,9 +36,9 @@ class PlayStartLogic
private const MIN_COIN_EXTRA = 100;
/** 豹子号中大奖额外平台币(无 BIGWIN 配置时兜底) */
private const SUPER_WIN_BONUS = 500;
/** 可触发超级大奖的 grid_number5=全1 10=全2 15=全3 20=全4 25=全5 30=全6;其中 5 和 30 固定 100% 出豹子 */
/** 可触发超级大奖的 grid_number5=全1 10=全2 15=全3 20=全4 25=全5 30=全6 */
private const SUPER_WIN_GRID_NUMBERS = [5, 10, 15, 20, 25, 30];
/** grid_number 为 5 30 时豹子概率固定 100%DiceRewardConfig tier=BIGWIN 约定) */
/** 5 30 抽到即豹子,不参与 BIGWIN 权重判定10/15/20/25 按 BIGWIN weight 判定是否豹子 */
private const SUPER_WIN_ALWAYS_GRID_NUMBERS = [5, 30];
/**
@@ -85,10 +85,9 @@ class PlayStartLogic
$safetyLine = (int) ($config->safety_line ?? 0);
$usePoolWeights = $poolProfit >= $safetyLine;
// 按上述规则抽取档位;若该档位无奖励或该方向下均无可用路径则重新摇取档位
// 按档位 T1-T5 抽取后,直接按该档位内 weight 抽取一条 DiceRewardConfig得到 grid_number
$maxTierRetry = 10;
$chosen = null;
$startCandidates = [];
$tier = null;
for ($tierAttempt = 0; $tierAttempt < $maxTierRetry; $tierAttempt++) {
$tier = $usePoolWeights
@@ -99,58 +98,55 @@ class PlayStartLogic
Log::warning("档位 {$tier} 无任何奖励配置,重新摇取档位");
continue;
}
$maxRewardRetry = count($tierRewards);
for ($attempt = 0; $attempt < $maxRewardRetry; $attempt++) {
try {
$chosen = self::drawRewardByWeight($tierRewards);
$chosenId = (int) ($chosen['id'] ?? 0);
if ($direction === 0) {
$startCandidates = DiceRewardConfig::getCachedBySEndIndex($chosenId);
} else {
$startCandidates = DiceRewardConfig::getCachedByNEndIndex($chosenId);
} catch (\RuntimeException $e) {
if ($e->getMessage() === self::EXCEPTION_WEIGHT_ALL_ZERO) {
Log::warning("档位 {$tier} 下所有奖励权重均为 0重新摇取档位");
continue;
}
if (!empty($startCandidates)) {
break 2;
}
Log::warning("方向 {$direction} 下无 s_end_index/n_end_index={$chosenId} 的配置,重新摇取");
throw $e;
}
Log::warning("方向 {$direction} 下档位 {$tier} 所有奖励均无可用路径配置,重新摇取档位");
break;
}
if (empty($startCandidates)) {
Log::error("方向 {$direction}多次摇取档位后仍无可用路径配置");
throw new ApiException('该方向下暂无可用路径配置');
if ($chosen === null) {
Log::error("多次摇取档位后仍无有效权重配置");
throw new ApiException('暂无可用奖励配置');
}
$chosenId = (int) ($chosen['id'] ?? 0);
$startRecord = $startCandidates[array_rand($startCandidates)];
$startIndex = (int) ($startRecord['id'] ?? 0);
$targetIndex = $direction === 0
? (int) ($startRecord['s_end_index'] ?? 0)
: (int) ($startRecord['n_end_index'] ?? 0);
$rollNumber = (int) ($startRecord['grid_number'] ?? 0);
// 起始索引:根据 direction 取 s_start_index(顺时针)或 n_start_index逆时针结束索引当前中奖配置 id
$startIndex = $direction === 0
? (int) ($chosen['s_start_index'] ?? 0)
: (int) ($chosen['n_start_index'] ?? 0);
$targetIndex = $chosenId;
$rollNumber = (int) ($chosen['grid_number'] ?? 0);
$realEv = (float) ($chosen['real_ev'] ?? 0);
$rewardWinCoin = 100 + $realEv; // 摇色子中奖平台币 = 100 + DiceRewardConfig.real_ev
$isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5';
// T5再来一次摇色子中奖平台币 = 配置的 real_ev其他档位 = 100 + real_ev
$rewardWinCoin = $isTierT5 ? $realEv : (100 + $realEv);
// 当抽到的 grid_number 为 5/10/15/20/25/30 时,可出豹子;其中 grid_number=5 30 固定 100% 豹子BIGWIN 约定
// 流程:先抽档位 T1-T5再按档位内权重抽色子点数5 30 抽到即豹子10/15/20/25 按 BIGWIN weight 判定是否中大奖(豹子如 [4,4,4,4,4]
$superWinCoin = 0;
$isWin = 0;
$bigWinRealEv = 0.0; // BIGWIN 档位的真实资金结算,用于从彩金池盈利中一并扣除
$bigWinRealEv = 0.0;
if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) {
$bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber);
$alwaysSuperWin = in_array($rollNumber, self::SUPER_WIN_ALWAYS_GRID_NUMBERS, true);
$doSuperWin = $alwaysSuperWin;
if (!$doSuperWin) {
$weight = $bigWinConfig !== null
? max(0.0, min(100.0, (float) ($bigWinConfig['weight'] ?? 0)))
: 100.0;
$roll = mt_rand(1, 10000) / 10000;
$doSuperWin = $roll <= $weight / 100;
? max(1, min(10000, (int) ($bigWinConfig['weight'] ?? 1)))
: 10000;
$roll = (random_int(0, PHP_INT_MAX - 1) / (float) PHP_INT_MAX);
$doSuperWin = $roll < ($weight / 10000.0);
}
if ($doSuperWin) {
$rollArray = $this->getSuperWinRollArray($rollNumber);
$isWin = 1;
if ($bigWinConfig !== null) {
$bigWinRealEv = (float) ($bigWinConfig['real_ev'] ?? 0);
$superWinCoin = 100 + $bigWinRealEv;
$superWinCoin = $bigWinRealEv;
} else {
$superWinCoin = self::SUPER_WIN_BONUS;
}
@@ -174,7 +170,6 @@ class PlayStartLogic
$configId = (int) $config->id;
$rewardId = $chosenId;
$configName = (string) ($config->name ?? '');
$isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5';
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
try {
Db::transaction(function () use (
@@ -322,34 +317,39 @@ class PlayStartLogic
return $arr;
}
/** 该组配置权重均为 0 时抛出,供调用方重试 */
private const EXCEPTION_WEIGHT_ALL_ZERO = 'REWARD_WEIGHT_ALL_ZERO';
/**
* T1-T5 档位内按 DiceRewardConfig.weight 抽取一条配置;权重均为 0 或未设时等权随机
* @param array<int, array> $rewards 该档位配置列表(每条含 weight、id、real_ev 等)
* @return array 抽中的一条配置
* 按权重抽取一条配置:仅 weight>0 参与抽取weight=0 不会被摇到)
* 使用 [0, total) 浮点随机,支持最小权重 0.1%(如 weight=0.1),避免整数随机导致小权重失真
* 全部 weight 为 0 时抛出 RuntimeException(EXCEPTION_WEIGHT_ALL_ZERO)
*/
private static function drawRewardByWeight(array $rewards): array
{
if (empty($rewards)) {
throw new \InvalidArgumentException('rewards 不能为空');
}
$weights = [];
$candidateWeights = [];
foreach ($rewards as $i => $row) {
$w = isset($row['weight']) ? max(0, (float) $row['weight']) : 0;
$weights[$i] = $w;
}
$total = array_sum($weights);
if ($total <= 0) {
return $rewards[array_rand($rewards)];
}
$r = mt_rand(1, (int) $total);
$acc = 0;
foreach ($weights as $i => $w) {
$acc += $w;
if ($r <= $acc) {
return $rewards[$i];
$w = isset($row['weight']) ? (float) $row['weight'] : 0.0;
if ($w > 0) {
$candidateWeights[$i] = $w;
}
}
return $rewards[array_key_last($rewards)];
$total = (float) array_sum($candidateWeights);
if ($total > 0) {
$r = (random_int(0, PHP_INT_MAX - 1) / (float) PHP_INT_MAX) * $total;
$acc = 0.0;
foreach ($candidateWeights as $i => $w) {
$acc += $w;
if ($r < $acc) {
return $rewards[$i];
}
}
return $rewards[array_key_last($candidateWeights)];
}
throw new \RuntimeException(self::EXCEPTION_WEIGHT_ALL_ZERO);
}
/**
@@ -368,7 +368,7 @@ class PlayStartLogic
if (empty($candidates)) {
break;
}
$idx = $candidates[array_rand($candidates)];
$idx = $candidates[random_int(0, count($candidates) - 1)];
$arr[$idx]++;
}
shuffle($arr);
@@ -403,8 +403,8 @@ class PlayStartLogic
$arr = $super;
$maxAttempts = 20;
for ($a = 0; $a < $maxAttempts; $a++) {
$idx = array_rand($arr);
$j = array_rand($arr);
$idx = random_int(0, count($arr) - 1);
$j = random_int(0, count($arr) - 1);
if ($idx === $j) {
$j = ($j + 1) % 5;
}

View File

@@ -119,9 +119,9 @@ class LotteryService
{
$total = array_sum($weights);
if ($total <= 0) {
return $tiers[array_rand($tiers)];
return $tiers[random_int(0, count($tiers) - 1)];
}
$r = mt_rand(1, $total);
$r = random_int(1, $total);
$acc = 0;
foreach ($weights as $i => $w) {
$acc += $w;
@@ -145,7 +145,7 @@ class LotteryService
return 0;
}
$total = $paid + $free;
$r = mt_rand(1, $total);
$r = random_int(1, $total);
return $r <= $paid ? 0 : 1;
}