'xsy','name'=>'小三元','type'=>1,'positions'=>[2,14,24]], ['key'=>'dsy','name'=>'大三元','type'=>2,'positions'=>[5,6,11,17,23]], ['key'=>'dsx','name'=>'大四喜','type'=>3,'positions'=>[10,23,11,5,17]], ['key'=>'tnsh','name'=>'天女散花','type'=>4,'positions'=>[1,7,13,19]], ['key'=>'tlbb','name'=>'天龙八部','type'=>5,'positions'=>[12,16,20,24]], ['key'=>'shuaidao','name'=>'甩刀','type'=>6,'positions'=>[8,9,20,21]], ['key'=>'huoche','name'=>'开火车','type'=>7,'positions'=>[3,4,15,16]], ['key'=>'dmg','name'=>'大满贯','type'=>8,'positions'=>range(1,24)], ]; foreach ($definitions as &$definition) { $key = $definition['key']; $definition['rate'] = max(0.0, (float)($settings['special_'.$key.'_rate'] ?? 0)); $definition['odds'] = max(0.0, (float)($settings['special_'.$key.'_odds'] ?? 0)); } unset($definition); return $definitions; } function game_board(array $settings): array { $odds = static fn(string $key, string $level, float $fallback): float => max(0.0, (float)($settings['odds_'.$key.'_'.$level] ?? $fallback)); return [ 1=>['fruit'=>13,'name'=>'橘子','multiplier'=>$odds('jz','high',10)], 2=>['fruit'=>2,'name'=>'铃铛','multiplier'=>$odds('ld','high',10)], 3=>['fruit'=>4,'name'=>'BAR','multiplier'=>$odds('bar','low',50)], 4=>['fruit'=>4,'name'=>'BAR','multiplier'=>$odds('bar','high',100)], 5=>['fruit'=>5,'name'=>'苹果','multiplier'=>$odds('pg','high',5)], 6=>['fruit'=>5,'name'=>'苹果','multiplier'=>$odds('pg','low',2)], 7=>['fruit'=>19,'name'=>'柠檬','multiplier'=>$odds('nm','high',10)], 8=>['fruit'=>8,'name'=>'西瓜','multiplier'=>$odds('xg','high',20)], 9=>['fruit'=>8,'name'=>'西瓜','multiplier'=>$odds('xg','low',2)], 10=>['fruit'=>0,'name'=>'LUCKY','multiplier'=>0], 11=>['fruit'=>5,'name'=>'苹果','multiplier'=>$odds('pg','high',5)], 12=>['fruit'=>13,'name'=>'橘子','multiplier'=>$odds('jz','low',2)], 13=>['fruit'=>13,'name'=>'橘子','multiplier'=>$odds('jz','high',10)], 14=>['fruit'=>2,'name'=>'铃铛','multiplier'=>$odds('ld','high',10)], 15=>['fruit'=>16,'name'=>'77','multiplier'=>$odds('77','low',2)], 16=>['fruit'=>16,'name'=>'77','multiplier'=>$odds('77','high',20)], 17=>['fruit'=>5,'name'=>'苹果','multiplier'=>$odds('pg','high',5)], 18=>['fruit'=>19,'name'=>'柠檬','multiplier'=>$odds('nm','low',2)], 19=>['fruit'=>19,'name'=>'柠檬','multiplier'=>$odds('nm','high',10)], 20=>['fruit'=>20,'name'=>'双星','multiplier'=>$odds('xx','high',20)], 21=>['fruit'=>20,'name'=>'双星','multiplier'=>$odds('xx','low',2)], 22=>['fruit'=>0,'name'=>'LUCKY','multiplier'=>0], 23=>['fruit'=>5,'name'=>'苹果','multiplier'=>$odds('pg','high',5)], 24=>['fruit'=>2,'name'=>'铃铛','multiplier'=>$odds('ld','low',2)], ]; } function game_window_stats(PDO $pdo, int $window): array { $window = min(100000, max(100, $window)); $sql = "SELECT COALESCE(SUM(bet_amount),0) AS bets, COALESCE(SUM(win_amount),0) AS wins, COUNT(*) AS rounds FROM (SELECT bet_amount, win_amount FROM game_logs WHERE game_type='spin' ORDER BY id DESC LIMIT ".$window.') recent'; $row = $pdo->query($sql)->fetch() ?: []; $bets = (float)($row['bets'] ?? 0); $wins = (float)($row['wins'] ?? 0); return ['bets'=>$bets,'wins'=>$wins,'rounds'=>(int)($row['rounds'] ?? 0),'rtp'=>$bets > 0 ? $wins / $bets : null]; } function game_weighted_pick(array $items, string $weightKey = 'weight'): array { $total = array_sum(array_map(static fn(array $item): float => max(0.0, (float)$item[$weightKey]), $items)); if ($total <= 0 || !$items) { throw new RuntimeException('无可用的开奖结果。'); } $roll = random_int(1, 1000000000) / 1000000000 * $total; foreach ($items as $item) { $roll -= max(0.0, (float)$item[$weightKey]); if ($roll <= 0) return $item; } return $items[array_key_last($items)]; } function game_position_weight(array $settings, int $position): float { return min(1000.0, max(0.01, (float)($settings['normal_weight_'.$position] ?? 1))); } function game_lucky_pick(array $settings): int { return (int)game_weighted_pick([ ['position'=>10, 'weight'=>game_position_weight($settings, 10)], ['position'=>22, 'weight'=>game_position_weight($settings, 22)], ])['position']; } function game_calibrated_ordinary_positions(array $groups, float $normalReturn, float $ordinaryProbability): array { $modelAtTilt = static function (float $tilt) use ($groups, $normalReturn): array { $positions = []; $total = 0.0; foreach ($groups as $group) { $maxMultiplier = max(array_column($group['positions'], 'multiplier')); $logs = []; foreach ($group['positions'] as $position => $entry) { $logs[$position] = log($entry['weight']) - $tilt * ($entry['multiplier'] / $maxMultiplier); } $maxLog = max($logs); $denominator = 0.0; $scores = []; foreach ($group['positions'] as $position => $entry) { $scores[$position] = exp($logs[$position] - $maxLog); $denominator += $scores[$position] * $entry['multiplier']; } if ($denominator <= 0) { throw new RuntimeException('每种普通图案至少需要一个大于 0 的赔率。'); } foreach ($group['positions'] as $position => $entry) { $probability = $normalReturn * $scores[$position] / $denominator; $positions[$position] = ['probability'=>$probability, 'weight'=>$entry['weight']]; $total += $probability; } } return ['positions'=>$positions, 'total'=>$total]; }; $low = -100.0; $high = 100.0; $lowModel = $modelAtTilt($low); $highModel = $modelAtTilt($high); if ($ordinaryProbability < $lowModel['total'] - 0.0000001 || $ordinaryProbability > $highModel['total'] + 0.0000001) { throw new RuntimeException('当前赔率无法同时满足目标 RTP 和 LUCKY 通吃率。'); } for ($iteration = 0; $iteration < 100; $iteration++) { $mid = ($low + $high) / 2; $model = $modelAtTilt($mid); if ($model['total'] < $ordinaryProbability) $low = $mid; else $high = $mid; } $tilt = ($low + $high) / 2; $model = $modelAtTilt($tilt); $model['tilt'] = $tilt; return $model; } function game_probability_model(array $settings, ?float $targetRtpPercent = null): array { $specials = game_special_definitions($settings); $specialRate = array_sum(array_column($specials, 'rate')) / 100; $luckyProbability = min(1.0, max(0.0, (float)($settings['lucky_eat_rate'] ?? 2) / 100)); if ($specialRate + $luckyProbability >= 1) { throw new RuntimeException('LUCKY 通吃率与特殊玩法总触发率之和必须小于 100%。'); } $specialExpected = 0.0; foreach ($specials as $special) { $specialExpected += ($special['rate'] / 100) * $special['odds']; } $target = min(2.0, max(0.01, ($targetRtpPercent ?? (float)($settings['target_rtp'] ?? 92)) / 100)); if ($target <= $specialExpected + 0.0000001) { throw new RuntimeException('目标 RTP 必须高于特殊玩法理论 RTP。'); } $board = game_board($settings); $groups = []; $luckyPositions = []; foreach ($board as $position => $cell) { $weight = game_position_weight($settings, $position); if ((int)$cell['fruit'] === 0) { $luckyPositions[$position] = $weight; continue; } $fruit = (int)$cell['fruit']; $groups[$fruit]['positions'][$position] = ['weight'=>$weight, 'multiplier'=>(float)$cell['multiplier']]; } if (!$luckyPositions) { throw new RuntimeException('游戏盘面缺少未中奖格。'); } $minCoefficient = 0.0; $maxCoefficient = 0.0; foreach ($groups as $group) { $multipliers = array_column($group['positions'], 'multiplier'); $positiveMultipliers = array_values(array_filter($multipliers, static fn(float $value): bool => $value > 0)); if (!$positiveMultipliers) { throw new RuntimeException('每种普通图案至少需要一个大于 0 的赔率。'); } $minCoefficient += 1 / max($positiveMultipliers); $minimum = min($multipliers); $maxCoefficient = $minimum <= 0 ? INF : $maxCoefficient + 1 / $minimum; } $normalProbability = 1 - $specialRate; $ordinaryProbability = $normalProbability - $luckyProbability; $minTarget = $specialExpected + (is_infinite($maxCoefficient) ? 0 : $ordinaryProbability / $maxCoefficient); $maxTarget = $specialExpected + $ordinaryProbability / $minCoefficient; if ($target < $minTarget - 0.0000001 || $target > $maxTarget + 0.0000001) { throw new RuntimeException('当前赔率和通吃率可支持的目标 RTP 范围为 '.number_format($minTarget * 100, 2).'% - '.number_format($maxTarget * 100, 2).'%。'); } $normalReturn = $target - $specialExpected; $calibrated = game_calibrated_ordinary_positions($groups, $normalReturn, $ordinaryProbability); $positions = $calibrated['positions']; $luckyWeightTotal = array_sum($luckyPositions); foreach ($luckyPositions as $position => $weight) { $positions[$position] = [ 'probability'=>$luckyProbability * $weight / $luckyWeightTotal, 'weight'=>$weight, ]; } ksort($positions); return [ 'positions'=>$positions, 'target_rtp'=>$target, 'min_rtp'=>$minTarget, 'max_rtp'=>$maxTarget, 'special_rate'=>$specialRate, 'special_expected'=>$specialExpected, 'normal_probability'=>$normalProbability, 'ordinary_probability'=>$ordinaryProbability, 'lucky_probability'=>$luckyProbability, 'calibration_tilt'=>$calibrated['tilt'], ]; } function game_effective_probability_model(array $settings, array $windowStats): array { $configuredTarget = min(2.0, max(0.01, (float)($settings['target_rtp'] ?? 92) / 100)); $baseModel = game_probability_model($settings); $effectiveTarget = $configuredTarget; if (($windowStats['rtp'] ?? null) !== null && (int)($windowStats['rounds'] ?? 0) >= 20) { $drift = max(-0.15, min(0.15, ($configuredTarget - (float)$windowStats['rtp']) * 0.5)); $effectiveTarget += $drift; } $effectiveTarget = max($baseModel['min_rtp'], min($baseModel['max_rtp'], $effectiveTarget)); $model = game_probability_model($settings, $effectiveTarget * 100); $model['configured_target_rtp'] = $configuredTarget; return $model; } function game_select_outcome(PDO $pdo, float $stake, array $bets, array $settings, ?array $windowStats = null): array { $specials = game_special_definitions($settings); $stats = $windowStats ?? game_window_stats($pdo, (int)($settings['rtp_window'] ?? 1000)); $model = game_effective_probability_model($settings, $stats); $specialRoll = random_int(1, 1000000) / 10000; $cursor = 0.0; foreach ($specials as $special) { $cursor += $special['rate']; if ($special['rate'] > 0 && $specialRoll <= $cursor) { if ($special['key'] === 'dsx' && random_int(0,1) === 1) { $special['positions'] = [22,11,23,17,5]; } return [ 'kind'=>'special','name'=>$special['name'],'type'=>$special['type'], 'position'=>game_lucky_pick($settings),'positions'=>$special['positions'], 'payout'=>round($stake * $special['odds'], 2),'multiplier'=>$special['odds'], 'special_rate'=>$special['rate'],'special_expected'=>$model['special_expected'], 'probability_model'=>'fixed_position_v2_lucky', ]; } } $board = game_board($settings); $candidates = []; foreach ($board as $position => $cell) { $bet = (float)($bets[$cell['fruit']] ?? 0); $payout = round($bet * (float)$cell['multiplier'], 2); $candidates[] = [ 'position'=>$position,'payout'=>$payout,'multiplier'=>(float)$cell['multiplier'], 'fruit'=>(int)$cell['fruit'],'name'=>$cell['name'], 'weight'=>$model['positions'][$position]['probability'], ]; } $choice = game_weighted_pick($candidates); if ($choice['fruit'] === 0) { return [ 'kind'=>'lucky_eat','name'=>'LUCKY通吃','type'=>9, 'position'=>$choice['position'],'positions'=>[], 'payout'=>0.0,'multiplier'=>0.0, 'special_expected'=>$model['special_expected'], 'position_probability'=>$model['positions'][$choice['position']]['probability'], 'effective_rtp'=>$model['target_rtp'], 'probability_model'=>'fixed_position_v2_lucky', ]; } return [ 'kind'=>'normal','name'=>$choice['name'],'type'=>0,'position'=>$choice['position'], 'positions'=>[$choice['position']],'payout'=>$choice['payout'], 'multiplier'=>$choice['multiplier'],'special_expected'=>$model['special_expected'], 'position_probability'=>$model['positions'][$choice['position']]['probability'], 'effective_rtp'=>$model['target_rtp'], 'probability_model'=>'fixed_position_v2_lucky', ]; } function game_theoretical_special_rtp(array $settings): float { $rtp = 0.0; foreach (game_special_definitions($settings) as $special) { $rtp += ($special['rate'] / 100) * $special['odds'] * 100; } return $rtp; }