- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController - 新增 Service 层: TransactionService, ReportService, FollowPlanService - pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll) - 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan) - 后台新增跟单计划管理 + 用户输赢明细统计 - 封盘状态显示倒计时 (x:xx) - 音效仅在开奖弹窗打开时播放 - 路由按模块分组整理 - autoload 支持 App\Services 命名空间
212 lines
8.1 KiB
PHP
212 lines
8.1 KiB
PHP
<?php
|
|
namespace App\Core;
|
|
|
|
class PK10Algorithm implements GameAlgorithmInterface {
|
|
|
|
/**
|
|
* 生成随机开奖结果 (1-10的排列)
|
|
*/
|
|
public static function generateResult(): array {
|
|
$cars = range(1, 10);
|
|
shuffle($cars);
|
|
return $cars; // index 0=冠军, 1=亚军, ..., 9=第十名
|
|
}
|
|
|
|
/**
|
|
* 带放水机制的开奖结果生成
|
|
*
|
|
* 算法策略:
|
|
* - targetProfitRate > 0 时:选择利润最接近「总投注额 × 目标盈利率」的结果
|
|
* - targetProfitRate == 0 时:选择平台利润最高的结果(原逻辑)
|
|
*
|
|
* @param array $bets 当期所有投注 [{bet_type, bet_target, amount, odds}, ...]
|
|
* @param array $waterConfig [{bet_type => win_rate_pct}, ...]
|
|
* 当前版本中 waterConfig 的 win_rate_pct 未直接参与结果选择,
|
|
* 因为开奖结果是全局排列,无法单独控制某个投注类型的胜率。
|
|
* 保留此参数供未来扩展(如按类型加权评分、分类型概率偏移等)。
|
|
* @param int $attempts 最大尝试次数
|
|
* @param float $targetProfitRate 目标盈利率(百分比,如 5.0 表示 5%)
|
|
*/
|
|
public static function generateControlledResult(array $bets, array $waterConfig, int $attempts = 100, float $targetProfitRate = 0): array {
|
|
if (empty($bets)) {
|
|
return self::generateResult();
|
|
}
|
|
|
|
// 计算目标利润(仅当 targetProfitRate > 0 时生效)
|
|
$targetProfit = 0;
|
|
if ($targetProfitRate > 0) {
|
|
$totalBet = 0;
|
|
foreach ($bets as $bet) {
|
|
$totalBet += (float)$bet['amount'];
|
|
}
|
|
$targetProfit = $totalBet * ($targetProfitRate / 100);
|
|
}
|
|
|
|
$bestResult = null;
|
|
$bestProfit = PHP_INT_MIN;
|
|
$bestDistance = PHP_FLOAT_MAX;
|
|
|
|
for ($i = 0; $i < $attempts; $i++) {
|
|
$result = self::generateResult();
|
|
$profit = self::calculatePlatformProfit($result, $bets);
|
|
|
|
if ($targetProfitRate > 0) {
|
|
// 目标盈利率模式:选择利润最接近目标值的结果
|
|
$distance = abs($profit - $targetProfit);
|
|
if ($distance < $bestDistance) {
|
|
$bestDistance = $distance;
|
|
$bestProfit = $profit;
|
|
$bestResult = $result;
|
|
}
|
|
} else {
|
|
// 原逻辑:选择平台利润最高的结果
|
|
if ($profit > $bestProfit) {
|
|
$bestProfit = $profit;
|
|
$bestResult = $result;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $bestResult;
|
|
}
|
|
|
|
/**
|
|
* 计算某个开奖结果下平台的利润
|
|
*/
|
|
public static function calculatePlatformProfit(array $result, array $bets): float {
|
|
$totalBet = 0;
|
|
$totalPayout = 0;
|
|
foreach ($bets as $bet) {
|
|
$amount = (float)$bet['amount'];
|
|
$totalBet += $amount;
|
|
if (self::checkWin($result, $bet['bet_type'], $bet['bet_target'])) {
|
|
$totalPayout += $amount + $amount * (float)$bet['odds'];
|
|
}
|
|
}
|
|
return $totalBet - $totalPayout;
|
|
}
|
|
|
|
/**
|
|
* 核心中奖判定
|
|
* @param array $result 开奖排名 [冠军车号, 亚军车号, ..., 第十名车号]
|
|
* @param string $betType 投注类型: rank/bs/oe/dt/sum/sum_bs
|
|
* @param string $betTarget 投注目标: rank1_5/rank1_big/dt1_dragon/sum_11/sum_big...
|
|
*/
|
|
public static function checkWin(array $result, string $betType, string $betTarget): bool {
|
|
switch ($betType) {
|
|
case 'rank': return self::checkRank($result, $betTarget);
|
|
case 'bs': return self::checkBigSmall($result, $betTarget);
|
|
case 'oe': return self::checkOddEven($result, $betTarget);
|
|
case 'dt': return self::checkDragonTiger($result, $betTarget);
|
|
case 'sum': return self::checkSum($result, $betTarget);
|
|
case 'sum_bs': return self::checkSumBigSmall($result, $betTarget);
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
// 名次投注: rank{N}_{carNo} 如 rank1_5 = 冠军是5号车
|
|
private static function checkRank(array $r, string $target): bool {
|
|
if (!preg_match('/^rank(\d+)_(\d+)$/', $target, $m)) return false;
|
|
$pos = (int)$m[1] - 1; // 0-indexed
|
|
$car = (int)$m[2];
|
|
return isset($r[$pos]) && $r[$pos] === $car;
|
|
}
|
|
|
|
// 大小: rank{N}_big/small, 车号>=6为大, <=5为小
|
|
private static function checkBigSmall(array $r, string $target): bool {
|
|
if (!preg_match('/^rank(\d+)_(big|small)$/', $target, $m)) return false;
|
|
$pos = (int)$m[1] - 1;
|
|
if (!isset($r[$pos])) return false;
|
|
$car = $r[$pos];
|
|
return $m[2] === 'big' ? $car >= 6 : $car <= 5;
|
|
}
|
|
|
|
// 单双: rank{N}_odd/even
|
|
private static function checkOddEven(array $r, string $target): bool {
|
|
if (!preg_match('/^rank(\d+)_(odd|even)$/', $target, $m)) return false;
|
|
$pos = (int)$m[1] - 1;
|
|
if (!isset($r[$pos])) return false;
|
|
$car = $r[$pos];
|
|
return $m[2] === 'odd' ? ($car % 2 === 1) : ($car % 2 === 0);
|
|
}
|
|
|
|
// 龙虎: dt{N}_dragon/tiger, N=1-5, 对应 1vs10, 2vs9, 3vs8, 4vs7, 5vs6
|
|
private static function checkDragonTiger(array $r, string $target): bool {
|
|
if (!preg_match('/^dt(\d)_(dragon|tiger)$/', $target, $m)) return false;
|
|
$pair = (int)$m[1]; // 1-5
|
|
$frontPos = $pair - 1; // 0,1,2,3,4
|
|
$backPos = 10 - $pair; // 9,8,7,6,5
|
|
if (!isset($r[$frontPos], $r[$backPos])) return false;
|
|
$front = $r[$frontPos];
|
|
$back = $r[$backPos];
|
|
if ($front === $back) return false; // 和局(PK10不会出现)
|
|
return $m[2] === 'dragon' ? $front > $back : $front < $back;
|
|
}
|
|
|
|
// 冠亚和值: sum_{3-19}
|
|
private static function checkSum(array $r, string $target): bool {
|
|
if (!preg_match('/^sum_(\d+)$/', $target, $m)) return false;
|
|
$sumVal = $r[0] + $r[1]; // 冠军+亚军
|
|
return $sumVal === (int)$m[1];
|
|
}
|
|
|
|
// 冠亚和大小单双: sum_big/small/odd/even, 和>=12为大, <=11为小
|
|
private static function checkSumBigSmall(array $r, string $target): bool {
|
|
$sumVal = $r[0] + $r[1];
|
|
switch ($target) {
|
|
case 'sum_big': return $sumVal >= 12;
|
|
case 'sum_small': return $sumVal <= 11;
|
|
case 'sum_odd': return $sumVal % 2 === 1;
|
|
case 'sum_even': return $sumVal % 2 === 0;
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取名次中文/英文名称
|
|
*/
|
|
public static function getRankName(int $pos): string {
|
|
$names = [1=>'champion',2=>'runner_up',3=>'rank_n',4=>'rank_n',5=>'rank_n',
|
|
6=>'rank_n',7=>'rank_n',8=>'rank_n',9=>'rank_n',10=>'rank_n'];
|
|
return $names[$pos] ?? 'rank_n';
|
|
}
|
|
|
|
/**
|
|
* 龙虎对应关系
|
|
*/
|
|
public static function getDragonTigerPairs(): array {
|
|
return [
|
|
1 => [1, 10], // 第1名 vs 第10名
|
|
2 => [2, 9],
|
|
3 => [3, 8],
|
|
4 => [4, 7],
|
|
5 => [5, 6],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 期号生成
|
|
*/
|
|
public static function generatePeriodNumber(int $gameId): string {
|
|
return 'PK' . $gameId . date('Ymd') . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
public static function getResultTable(): ?string { return 'pk10_results'; }
|
|
|
|
public static function formatResultForStorage(array $result, int $periodId): array {
|
|
return [
|
|
'period_id' => $periodId,
|
|
'rank_1' => $result[0], 'rank_2' => $result[1], 'rank_3' => $result[2],
|
|
'rank_4' => $result[3], 'rank_5' => $result[4], 'rank_6' => $result[5],
|
|
'rank_7' => $result[6], 'rank_8' => $result[7], 'rank_9' => $result[8],
|
|
'rank_10' => $result[9], 'champion_sum' => $result[0] + $result[1],
|
|
];
|
|
}
|
|
|
|
public static function parseResultFromDb(array $row): array {
|
|
$r = [];
|
|
for ($i = 1; $i <= 10; $i++) $r[] = (int)($row['rank_' . $i] ?? 0);
|
|
return $r;
|
|
}
|
|
}
|