coin; if ($ante <= 0) { throw new ApiException('ante must be a positive integer'); } // 注数合规校验:ante 必须存在于 dice_ante_config.mult $anteConfigModel = new DiceAnteConfig(); $exists = $anteConfigModel->where('mult', $ante)->count(); if ($exists <= 0) { throw new ApiException('当前注数不合规,请选择正确的注数'); } // 免费抽奖:优先使用 free_ticket(带 ante 与 count);兼容旧字段 free_ticket_count $freeTicket = $player->free_ticket ?? null; $freeTicketAnte = null; $freeTicketCount = 0; if (is_array($freeTicket)) { $a = $freeTicket['ante'] ?? null; $c = $freeTicket['count'] ?? null; if ($a !== null && $a !== '' && is_numeric($a)) { $freeTicketAnte = (int) $a; } if ($c !== null && $c !== '' && is_numeric($c)) { $freeTicketCount = (int) $c; } } $legacyFreeCount = (int) ($player->free_ticket_count ?? 0); $isFree = ($freeTicketAnte !== null && $freeTicketCount > 0) || $legacyFreeCount > 0; $ticketType = $isFree ? self::LOTTERY_TYPE_FREE : self::LOTTERY_TYPE_PAID; // 若为 free_ticket 免费抽奖:注数必须与券的 ante 一致 if ($ticketType === self::LOTTERY_TYPE_FREE && $freeTicketAnte !== null && $freeTicketCount > 0) { if ($ante !== $freeTicketAnte) { throw new ApiException('您有一张底注为' . $freeTicketAnte . '的免费抽奖券'); } } // 若为免费抽奖(旧逻辑):注数必须与上一次触发免费抽奖时的注数一致 if ($isFree) { $requiredAnte = Cache::get(self::FREE_ANTE_KEY_PREFIX . $playerId); if ($requiredAnte !== null && $requiredAnte !== '' && (int) $requiredAnte !== $ante) { throw new ApiException('免费抽奖注数必须与上一次一致,请修改注数后继续'); } } $configType0 = DiceLotteryPoolConfig::where('name', 'default')->find(); $configType1 = DiceLotteryPoolConfig::where('name', 'killScore')->find(); if (!$configType0) { throw new ApiException('Lottery pool config not found (name=default required)'); } // 余额校验:统一校验 ante * min(real_ev) $minEv = DiceRewardConfig::getCachedMinRealEv(); $needMinBalance = abs((float) $minEv) * $ante; if ($coin < $needMinBalance) { throw new ApiException('未达抽奖余额 ' . $needMinBalance . ',无法开始游戏'); } // 付费抽奖:开始前扣除费用 ante * UNIT_COST,不足则提示余额不足 $paidAmount = $ticketType === self::LOTTERY_TYPE_PAID ? round($ante * self::UNIT_COST, 2) : 0.0; if ($ticketType === self::LOTTERY_TYPE_PAID && $coin < $paidAmount) { throw new ApiException('余额不足'); } // 彩金池累计盈利:用于判断是否触发杀分(不再依赖单个玩家累计盈利) // 该值来自 dice_lottery_pool_config.profit_amount $poolProfitTotal = $configType0->profit_amount ?? 0; $safetyLine = (int) ($configType0->safety_line ?? 0); $killEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1; // 盈利>=安全线且开启杀分:付费/免费都用 killScore;盈利<安全线:付费用玩家权重,免费用 killScore(无则用 default) // 记录 lottery_config_id:用池权重时记对应池,付费用玩家权重时记 default $usePoolWeights = ($ticketType === self::LOTTERY_TYPE_PAID && $killEnabled && $poolProfitTotal >= $safetyLine && $configType1 !== null) || ($ticketType === self::LOTTERY_TYPE_FREE); $config = $usePoolWeights ? (($ticketType === self::LOTTERY_TYPE_FREE && $configType1 === null) ? $configType0 : $configType1) : $configType0; // 按档位 T1-T5 抽取后,从 DiceReward 表按当前方向取该档位数据,再按 weight 抽取一条得到 grid_number $rewardInstance = DiceReward::getCachedInstance(); $byTierDirection = $rewardInstance['by_tier_direction'] ?? []; $maxTierRetry = 10; $chosen = null; $tier = null; for ($tierAttempt = 0; $tierAttempt < $maxTierRetry; $tierAttempt++) { $tier = $usePoolWeights ? LotteryService::drawTierByWeights($config) : LotteryService::drawTierByPlayerWeights($player); $tierRewards = $byTierDirection[$tier][$direction] ?? []; if (empty($tierRewards)) { Log::warning("档位 {$tier} 方向 {$direction} 无任何 DiceReward,重新摇取档位"); continue; } if ($usePoolWeights) { $tierRewards = self::filterOutSuperWinOnlyGrids($tierRewards); if (empty($tierRewards)) { Log::warning("档位 {$tier} 方向 {$direction} 杀分档位下排除 5/30 后无可用奖励,重新摇取档位"); continue; } } try { $chosen = self::drawRewardByWeight($tierRewards); } catch (\RuntimeException $e) { if ($e->getMessage() === self::EXCEPTION_WEIGHT_ALL_ZERO) { Log::warning("档位 {$tier} 下所有奖励权重均为 0,重新摇取档位"); continue; } throw $e; } break; } if ($chosen === null) { Log::error("多次摇取档位后仍无有效 DiceReward"); throw new ApiException('No available reward config'); } $startIndex = (int) ($chosen['start_index'] ?? 0); $targetIndex = (int) ($chosen['end_index'] ?? 0); $rollNumber = (int) ($chosen['grid_number'] ?? 0); $realEv = (float) ($chosen['real_ev'] ?? 0); // T5/再来一次:以奖励行 tier 为准,并以摇奖档位 $tier 兜底(与 reward_tier 展示一致,避免 dice_reward 行缺 tier 时不发券) $isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5'; if ($isTierT5 === false && (string) ($tier ?? '') === 'T5') { $isTierT5 = true; } // 摇色子中奖:按 dice_reward_config.real_ev 直接结算(已乘 ante) $rewardWinCoin = round($realEv * $ante, 2); // 豹子判定:5/30 必豹子;10/15/20/25 按 DiceRewardConfig 中 BIGWIN 该点数的 weight 判定(0-10000,10000=100%) // 杀分档位:不触发豹子,5/30 已在上方抽取时排除,10/15/20/25 仅生成非豹子组合 $superWinCoin = 0.0; $isWin = 0; $bigWinRealEv = 0.0; if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) { if ($usePoolWeights) { // 杀分档位:绝不触发豹子,仅生成非豹子组合,不发放豹子奖金 $isWin = 0; $superWinCoin = 0.0; $rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber); } else { $bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber); $alwaysSuperWin = in_array($rollNumber, self::SUPER_WIN_ALWAYS_GRID_NUMBERS, true); $doSuperWin = $alwaysSuperWin; if (!$doSuperWin) { $bigWinWeight = 10000; if ($bigWinConfig !== null && isset($bigWinConfig['weight'])) { $bigWinWeight = min(10000, max(0, (int) $bigWinConfig['weight'])); $bigWinRealEv = (float) ($bigWinConfig['real_ev'] ?? 0); } $roll = (random_int(0, PHP_INT_MAX - 1) / (float) PHP_INT_MAX); $doSuperWin = $bigWinWeight > 0 && $roll < ($bigWinWeight / 10000.0); } else { if ($bigWinConfig !== null) { $bigWinRealEv = (float) ($bigWinConfig['real_ev'] ?? 0); } } if ($doSuperWin) { $rollArray = $this->getSuperWinRollArray($rollNumber); $isWin = 1; $bigWinEv = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS; $superWinCoin = round($bigWinEv * $ante, 2); // 中 BIGWIN 豹子:不走原奖励流程,不记录原奖励,不触发 T5 再来一次,仅发放豹子奖金 $rewardWinCoin = 0.0; $realEv = 0.0; $isTierT5 = false; } else { $rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber); } } } else { $rollArray = $this->generateRollArrayFromSum($rollNumber); } Log::info(sprintf( '摇取点数 roll_number=%d, 方向=%d, start_index=%d, target_index=%d', $rollNumber, $direction, $startIndex, $targetIndex )); $winCoin = round($superWinCoin + $rewardWinCoin, 2); // 赢取平台币 = 中大奖 + 摇色子中奖(豹子时 rewardWinCoin 已为 0) $record = null; $configId = (int) $config->id; $type0ConfigId = (int) $configType0->id; $rewardId = ($isWin === 1 && $superWinCoin > 0) ? 0 : $targetIndex; // 中豹子不记录原奖励配置 id $configName = (string) ($config->name ?? ''); $adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null; try { Db::transaction(function () use ( $playerId, $adminId, $configId, $type0ConfigId, $configName, $ticketType, $ante, $paidAmount, $winCoin, $superWinCoin, $rewardWinCoin, $isWin, $realEv, $bigWinRealEv, $direction, $startIndex, $targetIndex, $rollArray, $isTierT5, $tier, &$record ) { $rewardTier = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? ''); $record = DicePlayRecord::create([ 'player_id' => $playerId, 'admin_id' => $adminId, 'lottery_config_id' => $configId, 'lottery_type' => $ticketType, 'ante' => $ante, 'paid_amount' => $paidAmount, 'is_win' => $isWin, 'win_coin' => $winCoin, 'super_win_coin' => $superWinCoin, 'reward_win_coin' => $rewardWinCoin, 'direction' => $direction, 'reward_tier' => $rewardTier, 'start_index' => $startIndex, 'target_index' => $targetIndex, 'roll_array' => is_array($rollArray) ? json_encode($rollArray) : $rollArray, 'roll_number' => is_array($rollArray) ? array_sum($rollArray) : 0, 'status' => self::RECORD_STATUS_SUCCESS, ]); $p = DicePlayer::find($playerId); if (!$p) { throw new \RuntimeException('玩家不存在'); } $coinBefore = (float) $p->coin; // 开始前先扣付费金额,再加中奖金额(免费抽奖 paid_amount=0) $coinAfter = round($coinBefore - $paidAmount + $winCoin, 2); $p->coin = $coinAfter; // 免费抽奖消耗:优先消耗 free_ticket.count,耗尽则清空 free_ticket;否则兼容旧 free_ticket_count if ($ticketType === self::LOTTERY_TYPE_FREE) { $ft = $p->free_ticket ?? null; $ftAnte = null; $ftCount = 0; if (is_array($ft)) { $a = $ft['ante'] ?? null; $c = $ft['count'] ?? null; if ($a !== null && $a !== '' && is_numeric($a)) { $ftAnte = (int) $a; } if ($c !== null && $c !== '' && is_numeric($c)) { $ftCount = (int) $c; } } if ($ftAnte !== null && $ftCount > 0) { $next = $ftCount - 1; if ($next <= 0) { $p->free_ticket = null; } else { $p->free_ticket = ['ante' => $ftAnte, 'count' => $next]; } } else { $p->free_ticket_count = max(0, (int) $p->free_ticket_count - 1); } } // 记录每次游玩:写入抽奖券记录(用于后台“抽奖券记录”追踪付费/免费游玩与消耗) $isPaidPlay = $ticketType === self::LOTTERY_TYPE_PAID; $paidCnt = $isPaidPlay ? 1 : 0; $freeCnt = $isPaidPlay ? 0 : 1; DicePlayerTicketRecord::create([ 'player_id' => $playerId, 'admin_id' => $adminId, 'use_coins' => $paidAmount, 'ante' => $ante, 'total_ticket_count' => $paidCnt + $freeCnt, 'paid_ticket_count' => $paidCnt, 'free_ticket_count' => $freeCnt, 'remark' => ($isPaidPlay ? '付费游玩' : '免费游玩') . '|play_record_id=' . $record->id, ]); // 若本局中奖档位为 T5,则额外赠送 1 次免费抽奖次数: // - 新结构:写入 free_ticket(ante=本局注数,count+1) // - 兼容旧结构:free_ticket_count +1 if ($isTierT5) { $ft = $p->free_ticket ?? null; $ftAnte = null; $ftCount = 0; if (is_array($ft)) { $a = $ft['ante'] ?? null; $c = $ft['count'] ?? null; if ($a !== null && $a !== '' && is_numeric($a)) { $ftAnte = (int) $a; } if ($c !== null && $c !== '' && is_numeric($c)) { $ftCount = (int) $c; } } if ($ftAnte === null) { $ftAnte = $ante; } if ($ftAnte === $ante) { $p->free_ticket = ['ante' => $ante, 'count' => $ftCount + 1]; } else { // 若已有不同注数的免费券,则仍保留旧字段累加,避免覆盖玩家已有券 $p->free_ticket_count = (int) $p->free_ticket_count + 1; } DicePlayerTicketRecord::create([ 'player_id' => $playerId, 'admin_id' => $adminId, 'ante' => $ante, 'free_ticket_count' => 1, 'remark' => '中奖结果为T5', ]); // 记录免费抽奖注数,用于强制下一局注数一致 Cache::set(self::FREE_ANTE_KEY_PREFIX . $playerId, $ante, self::FREE_ANTE_TTL); } else { // 若本次消耗了最后一次免费抽奖,则清理注数锁 $ft = $p->free_ticket ?? null; $ftCount = 0; if (is_array($ft)) { $c = $ft['count'] ?? null; if ($c !== null && $c !== '' && is_numeric($c)) { $ftCount = (int) $c; } } if ($ticketType === self::LOTTERY_TYPE_FREE && $ftCount <= 0 && (int) $p->free_ticket_count <= 0) { Cache::delete(self::FREE_ANTE_KEY_PREFIX . $playerId); } } $p->save(); // 彩金池累计盈利累加在 name=default 彩金池上: // 付费:每局按「本局赢取平台币 win_coin - 抽奖费用 paid_amount(ante*UNIT_COST)」 // 免费券:paid_amount=0,只计入 win_coin $perPlayProfit = ($ticketType === self::LOTTERY_TYPE_PAID) ? ($winCoin - $paidAmount) : $winCoin; $addProfit = round($perPlayProfit, 2); try { DiceLotteryPoolConfig::where('id', $type0ConfigId)->update([ 'profit_amount' => Db::raw('IFNULL(profit_amount,0) + ' . sprintf('%.2f', $addProfit)), ]); } catch (\Throwable $e) { Log::warning('彩金池盈利累加失败', [ 'config_id' => $type0ConfigId, 'add_profit' => $addProfit, 'message' => $e->getMessage(), ]); } // 钱包流水拆分:先记录购券扣费,再记录抽奖结果(中奖/惩罚) if ($paidAmount > 0) { $walletAfterBuy = round($coinBefore - $paidAmount, 2); DicePlayerWalletRecord::create([ 'player_id' => $playerId, 'admin_id' => $adminId, 'coin' => round(-$paidAmount, 2), 'type' => self::WALLET_TYPE_BUY_DRAW, 'wallet_before' => $coinBefore, 'wallet_after' => $walletAfterBuy, 'remark' => '抽奖购券扣费|play_record_id=' . $record->id, ]); } $walletBeforeDraw = $coinBefore - $paidAmount; $drawRemark = ($winCoin >= 0 ? '抽奖中奖' : '抽奖惩罚') . '|play_record_id=' . $record->id; DicePlayerWalletRecord::create([ 'player_id' => $playerId, 'admin_id' => $adminId, 'coin' => $winCoin, 'type' => self::WALLET_TYPE_DRAW, 'wallet_before' => round($walletBeforeDraw, 2), 'wallet_after' => $coinAfter, 'remark' => $drawRemark, ]); }); } catch (\Throwable $e) { if ($record === null) { try { $record = DicePlayRecord::create([ 'player_id' => $playerId, 'admin_id' => $adminId ?? null, 'lottery_config_id' => $configId ?? 0, 'lottery_type' => $ticketType, 'is_win' => 0, 'win_coin' => 0, 'super_win_coin' => 0, 'reward_win_coin' => 0, 'direction' => $direction, 'reward_tier' => '', 'start_index' => $startIndex, 'target_index' => 0, 'roll_array' => '[]', 'roll_number' => 0, 'status' => self::RECORD_STATUS_TIMEOUT, ]); } catch (\Throwable $_) { // 表可能无 status 字段时忽略 } } throw $e; } $updated = DicePlayer::find($playerId); if ($updated) { UserCache::setUser($playerId, $updated->hidden(['password'])->toArray()); } if (!$record instanceof DicePlayRecord) { throw new \RuntimeException('对局记录创建失败'); } $arr = $record->toArray(); if (isset($arr['roll_array']) && is_string($arr['roll_array'])) { $arr['roll_array'] = json_decode($arr['roll_array'], true) ?? []; } $arr['roll_number'] = is_array($arr['roll_array'] ?? null) ? array_sum($arr['roll_array']) : 0; $arr['reward_tier'] = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? ''); // 记录完数据后返回当前玩家余额与抽奖次数 $arr['coin'] = $updated ? round((float) $updated->coin, 2) : 0.0; // 本局从玩家货币中扣除的金额:付费抽奖为 ante*UNIT_COST,免费抽奖为 0(与 paid_amount 一致) $arr['use_coin'] = round($paidAmount, 2); return $arr; } /** 该组配置权重均为 0 时抛出,供调用方重试 */ private const EXCEPTION_WEIGHT_ALL_ZERO = 'REWARD_WEIGHT_ALL_ZERO'; /** 杀分档位需排除的豹子号:5 和 30 只能组成豹子,无法生成非豹子组合 */ private const KILL_MODE_EXCLUDE_GRIDS = [5, 30]; /** * 杀分档位下排除 grid_number=5/30 的奖励(5/30 只能豹子,无法剔除) * @return array 排除后的奖励列表,保持索引连续 */ private static function filterOutSuperWinOnlyGrids(array $rewards): array { return array_values(array_filter($rewards, function ($r) { $g = (int) ($r['grid_number'] ?? 0); return !in_array($g, self::KILL_MODE_EXCLUDE_GRIDS, true); })); } /** * 按权重抽取一条配置:仅 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 不能为空'); } $candidateWeights = []; foreach ($rewards as $i => $row) { $w = isset($row['weight']) ? (float) $row['weight'] : 0.0; if ($w > 0) { $candidateWeights[$i] = $w; } } $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); } /** * 根据摇取点数(5-30)生成 5 个色子数组,每个 1-6,总和为 $sum * @return int[] 如 [1,2,3,4,5] */ private function generateRollArrayFromSum(int $sum): array { $sum = max(5, min(30, $sum)); $arr = [1, 1, 1, 1, 1]; $remain = $sum - 5; for ($i = 0; $i < $remain; $i++) { $candidates = array_keys(array_filter($arr, function ($v) { return $v < 6; })); if (empty($candidates)) { break; } $idx = $candidates[random_int(0, count($candidates) - 1)]; $arr[$idx]++; } shuffle($arr); return array_values($arr); } /** * 豹子组合:5->[1,1,1,1,1],10->[2,2,2,2,2],15->[3,3,3,3,3],20->[4,4,4,4,4],25->[5,5,5,5,5],30->[6,6,6,6,6] * @return int[] */ private function getSuperWinRollArray(int $gridNumber): array { if ($gridNumber === 30) { return array_fill(0, 5, 6); } $n = (int) ($gridNumber / 5); $n = max(1, min(5, $n)); return array_fill(0, 5, $n); } /** * 生成总和为 $sum 且非豹子的 5 个色子(1-6);sum=5 时仅 [1,1,1,1,1] 可能,仍返回该组合 * @return int[] */ private function generateNonSuperWinRollArrayWithSum(int $sum): array { $sum = max(5, min(30, $sum)); $super = $this->getSuperWinRollArray($sum); if ($sum === 5) { return $super; } $arr = $super; $maxAttempts = 20; for ($a = 0; $a < $maxAttempts; $a++) { $idx = random_int(0, count($arr) - 1); $j = random_int(0, count($arr) - 1); if ($idx === $j) { $j = ($j + 1) % 5; } $i = $idx; if ($arr[$i] >= 2 && $arr[$j] <= 5) { $arr[$i]--; $arr[$j]++; shuffle($arr); return array_values($arr); } if ($arr[$i] <= 5 && $arr[$j] >= 2) { $arr[$i]++; $arr[$j]--; shuffle($arr); return array_values($arr); } } return $this->generateRollArrayFromSum($sum); } /** * 模拟一局抽奖(不写库、不扣玩家),用于权重测试写入 dice_play_record_test * @param \app\dice\model\lottery_pool_config\DiceLotteryPoolConfig|null $config 奖池配置,自定义档位时可为 null * @param int $direction 0=顺时针 1=逆时针 * @param int $lotteryType 0=付费 1=免费 * @param int $ante 底注/注数(dice_ante_config.mult) * @param array|null $customTierWeights 自定义档位权重 ['T1'=>x, 'T2'=>x, ...],非空时忽略 config 的档位权重 * @return array 可直接用于 DicePlayRecordTest::create 的字段 + tier(用于统计档位概率) */ public function simulateOnePlay($config, int $direction, int $lotteryType = 0, int $ante = 1, ?array $customTierWeights = null): array { $rewardInstance = DiceReward::getCachedInstance(); $byTierDirection = $rewardInstance['by_tier_direction'] ?? []; $maxTierRetry = 10; $chosen = null; $tier = null; for ($tierAttempt = 0; $tierAttempt < $maxTierRetry; $tierAttempt++) { if ($customTierWeights !== null && $customTierWeights !== []) { $tier = LotteryService::drawTierByWeightsFromArray($customTierWeights); } else { if ($config === null) { throw new \RuntimeException('模拟抽奖:未提供奖池配置或自定义档位权重'); } $tier = LotteryService::drawTierByWeights($config); } $tierRewards = $byTierDirection[$tier][$direction] ?? []; if (empty($tierRewards)) { continue; } // 免费券或 killScore 池:与实际流程一致,排除 5/30 且不触发豹子 $useKillMode = ($lotteryType === 1) || ($config !== null && (string) ($config->name ?? '') === 'killScore'); if ($useKillMode) { $tierRewards = self::filterOutSuperWinOnlyGrids($tierRewards); if (empty($tierRewards)) { continue; } } try { $chosen = self::drawRewardByWeight($tierRewards); } catch (\RuntimeException $e) { if ($e->getMessage() === self::EXCEPTION_WEIGHT_ALL_ZERO) { continue; } throw $e; } break; } if ($chosen === null) { throw new \RuntimeException('模拟抽奖:无可用奖励配置'); } $startIndex = (int) ($chosen['start_index'] ?? 0); $targetIndex = (int) ($chosen['end_index'] ?? 0); $rollNumber = (int) ($chosen['grid_number'] ?? 0); $realEv = (float) ($chosen['real_ev'] ?? 0); // 摇色子中奖:按 real_ev 直接结算(与正式抽奖 run() 一致) $rewardWinCoin = round($realEv * $ante, 2); $superWinCoin = 0.0; $isWin = 0; $bigWinRealEv = 0.0; if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) { if ($useKillMode) { // 杀分档位:绝不触发豹子,仅生成非豹子组合,不发放豹子奖金 $isWin = 0; $superWinCoin = 0.0; $rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber); } else { $bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber); $alwaysSuperWin = in_array($rollNumber, self::SUPER_WIN_ALWAYS_GRID_NUMBERS, true); $doSuperWin = $alwaysSuperWin; if (!$doSuperWin) { $bigWinWeight = 10000; if ($bigWinConfig !== null && isset($bigWinConfig['weight'])) { $bigWinWeight = min(10000, max(0, (int) $bigWinConfig['weight'])); } $roll = (random_int(0, PHP_INT_MAX - 1) / (float) PHP_INT_MAX); $doSuperWin = $bigWinWeight > 0 && $roll < ($bigWinWeight / 10000.0); } if ($bigWinConfig !== null && isset($bigWinConfig['real_ev'])) { $bigWinRealEv = (float) $bigWinConfig['real_ev']; } if ($doSuperWin) { $rollArray = $this->getSuperWinRollArray($rollNumber); $isWin = 1; $bigWinEv = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS; $superWinCoin = round($bigWinEv * $ante, 2); $rewardWinCoin = 0.0; // 中豹子时不走原奖励流程 $realEv = 0.0; } else { $rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber); } } } else { $rollArray = $this->generateRollArrayFromSum($rollNumber); } $winCoin = round($superWinCoin + $rewardWinCoin, 2); $configId = $config !== null ? (int) $config->id : 0; $configName = $config !== null ? (string) ($config->name ?? '') : '自定义'; $costRealEv = $realEv + ($isWin === 1 ? $bigWinRealEv : 0.0); $paidAmount = $lotteryType === 0 ? round($ante * self::UNIT_COST, 2) : 0.0; $rewardTier = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? ''); // 与写入记录的 reward_tier 完全一致:仅当展示档位为 T5(非豹子大奖)时触发「再来一次」链式免费局 $grantsFreeTicket = ($rewardTier === 'T5'); return [ 'player_id' => 0, 'admin_id' => 0, 'lottery_config_id' => $configId, 'lottery_type' => $lotteryType, 'is_win' => $isWin, 'win_coin' => $winCoin, 'ante' => $ante, 'paid_amount' => $paidAmount, 'super_win_coin' => $superWinCoin, 'reward_win_coin' => $rewardWinCoin, 'direction' => $direction, 'reward_tier' => $rewardTier, 'start_index' => $startIndex, 'target_index' => $targetIndex, 'roll_array' => json_encode($rollArray), 'roll_number' => array_sum($rollArray), 'status' => self::RECORD_STATUS_SUCCESS, 'tier' => $tier, 'roll_number_for_count' => $rollNumber, 'real_ev' => $realEv, 'bigwin_real_ev' => $isWin === 1 ? $bigWinRealEv : 0.0, 'cost_ev' => $costRealEv, 'grants_free_ticket' => $grantsFreeTicket, ]; } }