从新设计抽奖逻辑

This commit is contained in:
2026-03-06 16:02:17 +08:00
parent 931af70c36
commit 27f95a303a
4 changed files with 198 additions and 61 deletions

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -94,6 +94,29 @@ class LotteryService
(int) ($config->t4_wight ?? 0),
(int) ($config->t5_wight ?? 0),
];
return self::drawTierByWeightArray($tiers, $weights);
}
/**
* 根据玩家 t1_wightt5_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)];

View File

@@ -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<int, 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<int, 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<int, 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;
}
/** 保存后刷新缓存 */