diff --git a/server/app/api/controller/GameController.php b/server/app/api/controller/GameController.php index cd8dc27..c188d6e 100644 --- a/server/app/api/controller/GameController.php +++ b/server/app/api/controller/GameController.php @@ -97,6 +97,7 @@ class GameController extends OpenController return $this->fail($e->getMessage(), ReturnCode::BUSINESS_ERROR); } catch (\Throwable $e) { $timeoutRecord = null; + $timeout_message = ''; try { $timeoutRecord = DicePlayRecord::create([ 'player_id' => $userId, @@ -116,7 +117,8 @@ class GameController extends OpenController } $payload = $timeoutRecord ? ['record' => $timeoutRecord->toArray()] : []; - return $this->success($payload, '服务超时,'.$timeout_message ?? '没有原因'); + $msg = $timeout_message !== '' ? $timeout_message : '没有原因'; + return $this->fail('服务超时,' . $msg); } } } diff --git a/server/app/api/logic/PlayStartLogic.php b/server/app/api/logic/PlayStartLogic.php index 924ff04..6bc5adb 100644 --- a/server/app/api/logic/PlayStartLogic.php +++ b/server/app/api/logic/PlayStartLogic.php @@ -70,35 +70,47 @@ class PlayStartLogic throw new ApiException('奖池配置不存在'); } - // 先按奖池权重抽出档位 T1-T5 - $tier = LotteryService::drawTierByWeights($config); - - // 生成 5 个 1-6 的点数,计算总和 roll_number(即本局摇到的点数) - $rollArray = $this->generateRollArray(); - $rollNumber = (int) array_sum($rollArray); - - // 索引范围为 0~25 共 26 个格子 - $boardSize = 26; - - // 1. 根据抽到的档位,在 tier 相等的数据中任选一条,其 id 为结束索引 target_index(从缓存读取) - $tierRewards = DiceRewardConfig::getCachedByTier($tier); - if (empty($tierRewards)) { - Log::error("档位 {$tier} 无任何奖励配置"); - throw new ApiException('该档位暂无奖励配置'); + // 按玩家权重抽取档位;若该档位无奖励或该方向下均无可用路径则重新摇取档位 + $maxTierRetry = 10; + $chosen = null; + $startCandidates = []; + $tier = null; + for ($tierAttempt = 0; $tierAttempt < $maxTierRetry; $tierAttempt++) { + $tier = LotteryService::drawTierByPlayerWeights($player); + $tierRewards = DiceRewardConfig::getCachedByTier($tier); + if (empty($tierRewards)) { + Log::warning("档位 {$tier} 无任何奖励配置,重新摇取档位"); + continue; + } + $maxRewardRetry = count($tierRewards); + for ($attempt = 0; $attempt < $maxRewardRetry; $attempt++) { + $chosen = $tierRewards[array_rand($tierRewards)]; + $chosenId = (int) ($chosen['id'] ?? 0); + if ($direction === 0) { + $startCandidates = DiceRewardConfig::getCachedBySEndIndex($chosenId); + } else { + $startCandidates = DiceRewardConfig::getCachedByNEndIndex($chosenId); + } + if (!empty($startCandidates)) { + break 2; + } + Log::warning("方向 {$direction} 下无 s_end_index/n_end_index={$chosenId} 的配置,重新摇取"); + } + Log::warning("方向 {$direction} 下档位 {$tier} 所有奖励均无可用路径配置,重新摇取档位"); } - $chosen = $tierRewards[array_rand($tierRewards)]; - $targetIndex = (int) ($chosen['id'] ?? 0); - $targetIndex = (($targetIndex % $boardSize) + $boardSize) % $boardSize; - - // 2. 根据结果反推起始点 start_index(由 target_index 与方向反算) - // 顺时针(direction=0): targetIndex = (startIndex + rollNumber) % 26 => startIndex = (targetIndex - rollNumber) % 26 - // 逆时针(direction=1): targetIndex = (startIndex - rollNumber) % 26 => startIndex = (targetIndex + rollNumber) % 26 - if ($direction === 0) { - $startIndex = ($targetIndex - $rollNumber) % $boardSize; - } else { - $startIndex = ($targetIndex + $rollNumber) % $boardSize; + if (empty($startCandidates)) { + Log::error("方向 {$direction} 下多次摇取档位后仍无可用路径配置"); + throw new ApiException('该方向下暂无可用路径配置'); } - $startIndex = ($startIndex % $boardSize + $boardSize) % $boardSize; + $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); + $rollArray = $this->generateRollArrayFromSum($rollNumber); Log::info(sprintf( '摇取点数 roll_number=%d, 方向=%d, start_index=%d, target_index=%d', @@ -112,7 +124,7 @@ class PlayStartLogic $record = null; $configId = (int) $config->id; - $rewardId = (int) ($chosen['id'] ?? 0); + $rewardId = $chosenId; $configName = (string) ($config->name ?? ''); $isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5'; try { @@ -224,16 +236,30 @@ class PlayStartLogic 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; return $arr; } - /** 生成 5 个 1-6 的点数,roll_number 为其总和 */ - private function generateRollArray(): array + /** + * 根据摇取点数(5-30)生成 5 个色子数组,每个 1-6,总和为 $sum + * @return int[] 如 [1,2,3,4,5] + */ + private function generateRollArrayFromSum(int $sum): array { - $dice = []; - for ($i = 0; $i < 5; $i++) { - $dice[] = random_int(1, 6); + $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[array_rand($candidates)]; + $arr[$idx]++; } - return $dice; + shuffle($arr); + return array_values($arr); } } diff --git a/server/app/api/service/LotteryService.php b/server/app/api/service/LotteryService.php index e411221..d2b4483 100644 --- a/server/app/api/service/LotteryService.php +++ b/server/app/api/service/LotteryService.php @@ -94,6 +94,29 @@ class LotteryService (int) ($config->t4_wight ?? 0), (int) ($config->t5_wight ?? 0), ]; + return self::drawTierByWeightArray($tiers, $weights); + } + + /** + * 根据玩家 t1_wight~t5_wight 权重随机抽取中奖档位 T1-T5 + * t1_wight=T1, t2_wight=T2, t3_wight=T3, t4_wight=T4, t5_wight=T5 + */ + public static function drawTierByPlayerWeights(DicePlayer $player): string + { + $tiers = ['T1', 'T2', 'T3', 'T4', 'T5']; + $weights = [ + (int) ($player->t1_wight ?? 0), + (int) ($player->t2_wight ?? 0), + (int) ($player->t3_wight ?? 0), + (int) ($player->t4_wight ?? 0), + (int) ($player->t5_wight ?? 0), + ]; + return self::drawTierByWeightArray($tiers, $weights); + } + + /** 按档位权重数组抽取 T1-T5 */ + private static function drawTierByWeightArray(array $tiers, array $weights): string + { $total = array_sum($weights); if ($total <= 0) { return $tiers[array_rand($tiers)]; diff --git a/server/app/dice/model/reward_config/DiceRewardConfig.php b/server/app/dice/model/reward_config/DiceRewardConfig.php index b840363..1d17517 100644 --- a/server/app/dice/model/reward_config/DiceRewardConfig.php +++ b/server/app/dice/model/reward_config/DiceRewardConfig.php @@ -20,18 +20,23 @@ use support\think\Cache; * @property $ui_text 前端显示文本 * @property $real_ev 真实资金结算 * @property $tier 所属档位 + * @property $s_end_index 顺时针结束索引 + * @property $n_end_index 逆时针结束索引 * @property $remark 备注 * @property $create_time 创建时间 * @property $update_time 修改时间 */ class DiceRewardConfig extends BaseModel { - /** 缓存键:全玩家通用的奖励配置列表 */ - private const CACHE_KEY_LIST = 'dice:reward_config:list'; + /** 缓存键:彩金池奖励列表实例(含列表与索引) */ + private const CACHE_KEY_INSTANCE = 'dice:reward_config:instance'; /** 缓存过期时间(秒),保存时会主动刷新故设较长 */ private const CACHE_TTL = 86400 * 30; + /** 当前请求内已加载的实例,避免同请求多次读缓存 */ + private static ?array $instance = null; + /** * 数据表主键 * @var string @@ -44,28 +49,89 @@ class DiceRewardConfig extends BaseModel */ protected $table = 'dice_reward_config'; + /** + * 获取彩金池实例(含 list / 索引),无则从库加载并写入缓存;同请求内复用 + * @return array{list: array, by_tier: array, by_s_end_index: array, by_n_end_index: array, min_real_ev: float} + */ + public static function getCachedInstance(): array + { + if (self::$instance !== null) { + return self::$instance; + } + $instance = Cache::get(self::CACHE_KEY_INSTANCE); + if ($instance !== null && is_array($instance)) { + self::$instance = $instance; + return $instance; + } + self::refreshCache(); + $instance = Cache::get(self::CACHE_KEY_INSTANCE); + self::$instance = is_array($instance) ? $instance : self::buildEmptyInstance(); + return self::$instance; + } + /** * 获取缓存的奖励列表(无则从库加载并写入缓存) * @return array */ public static function getCachedList(): array { - $list = Cache::get(self::CACHE_KEY_LIST); - if ($list !== null && is_array($list)) { - return $list; - } - self::refreshCache(); - $list = Cache::get(self::CACHE_KEY_LIST); - return is_array($list) ? $list : []; + $inst = self::getCachedInstance(); + return $inst['list'] ?? []; } /** - * 重新从数据库加载并写入缓存(保存时调用) + * 重新从数据库加载并写入缓存(保存时调用),构建列表与索引 */ public static function refreshCache(): void { $list = (new self())->order('id', 'asc')->select()->toArray(); - Cache::set(self::CACHE_KEY_LIST, $list, self::CACHE_TTL); + $byTier = []; + $bySEndIndex = []; + $byNEndIndex = []; + foreach ($list as $row) { + $tier = isset($row['tier']) ? (string) $row['tier'] : ''; + if ($tier !== '') { + if (!isset($byTier[$tier])) { + $byTier[$tier] = []; + } + $byTier[$tier][] = $row; + } + $sEnd = isset($row['s_end_index']) ? (int) $row['s_end_index'] : 0; + if ($sEnd !== 0) { + if (!isset($bySEndIndex[$sEnd])) { + $bySEndIndex[$sEnd] = []; + } + $bySEndIndex[$sEnd][] = $row; + } + $nEnd = isset($row['n_end_index']) ? (int) $row['n_end_index'] : 0; + if ($nEnd !== 0) { + if (!isset($byNEndIndex[$nEnd])) { + $byNEndIndex[$nEnd] = []; + } + $byNEndIndex[$nEnd][] = $row; + } + } + $minRealEv = empty($list) ? 0.0 : (float) min(array_column($list, 'real_ev')); + self::$instance = [ + 'list' => $list, + 'by_tier' => $byTier, + 'by_s_end_index' => $bySEndIndex, + 'by_n_end_index' => $byNEndIndex, + 'min_real_ev' => $minRealEv, + ]; + Cache::set(self::CACHE_KEY_INSTANCE, self::$instance, self::CACHE_TTL); + } + + /** 空实例结构 */ + private static function buildEmptyInstance(): array + { + return [ + 'list' => [], + 'by_tier' => [], + 'by_s_end_index' => [], + 'by_n_end_index' => [], + 'min_real_ev' => 0.0, + ]; } /** @@ -73,13 +139,8 @@ class DiceRewardConfig extends BaseModel */ public static function getCachedMinRealEv(): float { - $list = self::getCachedList(); - if (empty($list)) { - return 0.0; - } - $vals = array_column($list, 'real_ev'); - $min = min($vals); - return (float) $min; + $inst = self::getCachedInstance(); + return (float) ($inst['min_real_ev'] ?? 0.0); } /** @@ -88,14 +149,39 @@ class DiceRewardConfig extends BaseModel */ public static function getCachedByTier(string $tier): array { - $list = self::getCachedList(); - $rows = []; - foreach ($list as $row) { - if (isset($row['tier']) && (string) $row['tier'] === $tier) { - $rows[] = $row; - } - } - return $rows; + $inst = self::getCachedInstance(); + $byTier = $inst['by_tier'] ?? []; + return $byTier[$tier] ?? []; + } + + /** + * 从缓存按顺时针结束索引取列表(s_end_index = id 的配置) + * @return array + */ + public static function getCachedBySEndIndex(int $id): array + { + $inst = self::getCachedInstance(); + $by = $inst['by_s_end_index'] ?? []; + return $by[$id] ?? []; + } + + /** + * 从缓存按逆时针结束索引取列表(n_end_index = id 的配置) + * @return array + */ + public static function getCachedByNEndIndex(int $id): array + { + $inst = self::getCachedInstance(); + $by = $inst['by_n_end_index'] ?? []; + return $by[$id] ?? []; + } + + /** + * 清除当前请求内实例(如测试或需强制下次读缓存时调用) + */ + public static function clearRequestInstance(): void + { + self::$instance = null; } /** 保存后刷新缓存 */