[色子游戏]奖励配置权重测试记录
This commit is contained in:
@@ -6,10 +6,12 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\reward_config;
|
||||
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use app\dice\model\reward_config\DiceRewardConfig;
|
||||
use app\dice\model\reward_config\DiceRewardConfigRecord;
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\utils\Helper;
|
||||
use app\dice\model\reward_config\DiceRewardConfig;
|
||||
use support\Log;
|
||||
|
||||
/**
|
||||
@@ -135,4 +137,160 @@ class DiceRewardConfigLogic extends BaseLogic
|
||||
}
|
||||
DiceRewardConfig::refreshCache();
|
||||
}
|
||||
|
||||
/** 测试时档位权重均为 0 的异常标识 */
|
||||
private const EXCEPTION_WEIGHT_ALL_ZERO = 'REWARD_WEIGHT_ALL_ZERO';
|
||||
|
||||
/**
|
||||
* 按权重抽取一条配置(与 PlayStartLogic 抽奖逻辑一致,仅 weight>0 参与)
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按档位权重数组抽取 T1-T5
|
||||
*/
|
||||
private static function drawTierByWeightArray(array $tiers, array $weights): string
|
||||
{
|
||||
$total = array_sum($weights);
|
||||
if ($total <= 0) {
|
||||
return $tiers[random_int(0, count($tiers) - 1)];
|
||||
}
|
||||
$r = random_int(1, (int) $total);
|
||||
$acc = 0;
|
||||
foreach ($weights as $i => $w) {
|
||||
$acc += (int) $w;
|
||||
if ($r <= $acc) {
|
||||
return $tiers[$i];
|
||||
}
|
||||
}
|
||||
return $tiers[count($tiers) - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行权重配比测试:仅按当前配置在内存中模拟 N 次抽奖,统计各 grid_number 落点数量。
|
||||
* 不创建任何游玩记录(DicePlayRecord)、不扣券、不写钱包,仅用于验证权重配比效果。
|
||||
*
|
||||
* @param int $testCount 测试次数 100/500/1000/5000/10000
|
||||
* @param bool $saveRecord 是否保存到 dice_reward_config_record(测试记录表,非游玩记录)
|
||||
* @param int|null $adminId 执行人管理员ID
|
||||
* @param int|null $lotteryConfigId 奖池配置ID(DiceLotteryPoolConfig),用于设定 T1-T5 档位概率;不传则使用 type=0 的配置或均等
|
||||
* @return array{counts: array<int,int>, record_id: int|null} counts 为 grid_number=>出现次数
|
||||
*/
|
||||
public function runWeightTest(int $testCount, bool $saveRecord = true, ?int $adminId = null, ?int $lotteryConfigId = null): array
|
||||
{
|
||||
$allowedCounts = [100, 500, 1000, 5000, 10000];
|
||||
if (!in_array($testCount, $allowedCounts, true)) {
|
||||
throw new ApiException('测试次数仅支持 100、500、1000、5000、10000');
|
||||
}
|
||||
|
||||
$grouped = $this->getListGroupedByTier();
|
||||
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
||||
$tierWeights = [1, 1, 1, 1, 1];
|
||||
$config = null;
|
||||
if ($lotteryConfigId !== null && $lotteryConfigId > 0) {
|
||||
$config = DiceLotteryPoolConfig::find($lotteryConfigId);
|
||||
}
|
||||
if (!$config) {
|
||||
$config = DiceLotteryPoolConfig::where('type', 0)->find();
|
||||
}
|
||||
if ($config) {
|
||||
$tierWeights = [
|
||||
(int) ($config->t1_weight ?? 0),
|
||||
(int) ($config->t2_weight ?? 0),
|
||||
(int) ($config->t3_weight ?? 0),
|
||||
(int) ($config->t4_weight ?? 0),
|
||||
(int) ($config->t5_weight ?? 0),
|
||||
];
|
||||
if (array_sum($tierWeights) <= 0) {
|
||||
$tierWeights = [1, 1, 1, 1, 1];
|
||||
}
|
||||
}
|
||||
|
||||
$counts = [];
|
||||
$maxRetry = 20;
|
||||
for ($i = 0; $i < $testCount; $i++) {
|
||||
$tier = self::drawTierByWeightArray($tiers, $tierWeights);
|
||||
$rewards = $grouped[$tier] ?? [];
|
||||
if (empty($rewards)) {
|
||||
continue;
|
||||
}
|
||||
$attempt = 0;
|
||||
while ($attempt < $maxRetry) {
|
||||
try {
|
||||
$chosen = self::drawRewardByWeight($rewards);
|
||||
$gridNumber = isset($chosen['grid_number']) ? (int) $chosen['grid_number'] : 0;
|
||||
if ($gridNumber >= 5 && $gridNumber <= 30) {
|
||||
$counts[$gridNumber] = ($counts[$gridNumber] ?? 0) + 1;
|
||||
}
|
||||
break;
|
||||
} catch (\RuntimeException $e) {
|
||||
if ($e->getMessage() === self::EXCEPTION_WEIGHT_ALL_ZERO) {
|
||||
$attempt++;
|
||||
continue;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$snapshot = [];
|
||||
foreach ($grouped as $tierKey => $rows) {
|
||||
foreach ($rows as $row) {
|
||||
$snapshot[] = [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'grid_number' => (int) ($row['grid_number'] ?? 0),
|
||||
'tier' => (string) ($row['tier'] ?? ''),
|
||||
'weight' => (int) ($row['weight'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$tierWeightsSnapshot = [
|
||||
'T1' => $tierWeights[0] ?? 0,
|
||||
'T2' => $tierWeights[1] ?? 0,
|
||||
'T3' => $tierWeights[2] ?? 0,
|
||||
'T4' => $tierWeights[3] ?? 0,
|
||||
'T5' => $tierWeights[4] ?? 0,
|
||||
];
|
||||
$recordId = null;
|
||||
if ($saveRecord) {
|
||||
$record = new DiceRewardConfigRecord();
|
||||
$record->test_count = $testCount;
|
||||
$record->weight_config_snapshot = $snapshot;
|
||||
$record->tier_weights_snapshot = $tierWeightsSnapshot;
|
||||
$record->lottery_config_id = $config ? (int) $config->id : null;
|
||||
$record->result_counts = $counts;
|
||||
$record->admin_id = $adminId;
|
||||
$record->create_time = date('Y-m-d H:i:s');
|
||||
$record->save();
|
||||
$recordId = (int) $record->id;
|
||||
}
|
||||
|
||||
return ['counts' => $counts, 'record_id' => $recordId];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\reward_config_record;
|
||||
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use app\dice\model\reward_config\DiceRewardConfig;
|
||||
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
|
||||
/**
|
||||
* 奖励配置权重测试记录逻辑层
|
||||
*/
|
||||
class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new DiceRewardConfigRecord();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表,并为每条记录附加 admin_name(管理员姓名:realname 或 username)
|
||||
*/
|
||||
public function getList($query): mixed
|
||||
{
|
||||
$result = parent::getList($query);
|
||||
if (!is_array($result)) {
|
||||
return $result;
|
||||
}
|
||||
$rows = $result['data'] ?? $result['records'] ?? null;
|
||||
if (!is_array($rows) || empty($rows)) {
|
||||
return $result;
|
||||
}
|
||||
$adminIds = array_unique(array_filter(array_column($rows, 'admin_id')));
|
||||
$nameMap = $this->getAdminNameMap($adminIds);
|
||||
$key = isset($result['data']) ? 'data' : 'records';
|
||||
foreach ($result[$key] as &$row) {
|
||||
$aid = isset($row['admin_id']) ? (int) $row['admin_id'] : 0;
|
||||
$row['admin_name'] = $nameMap[$aid] ?? ($aid > 0 ? '' : '—');
|
||||
}
|
||||
unset($row);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据管理员 ID 列表获取 id => 姓名(realname 优先,否则 username)
|
||||
* @param array $adminIds
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function getAdminNameMap(array $adminIds): array
|
||||
{
|
||||
if (empty($adminIds)) {
|
||||
return [];
|
||||
}
|
||||
$list = SystemUser::whereIn('id', $adminIds)->field('id,realname,username')->select()->toArray();
|
||||
$map = [];
|
||||
foreach ($list as $user) {
|
||||
$user = is_array($user) ? $user : (array) $user;
|
||||
$id = (int) ($user['id'] ?? 0);
|
||||
$name = trim((string) ($user['realname'] ?? ''));
|
||||
if ($name === '') {
|
||||
$name = trim((string) ($user['username'] ?? ''));
|
||||
}
|
||||
$map[$id] = $name !== '' ? $name : (string) $id;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将测试记录的权重导入到正式配置:weight_config_snapshot → DiceRewardConfig,tier_weights_snapshot → DiceLotteryPoolConfig,并刷新缓存
|
||||
* @param int $recordId 测试记录 ID
|
||||
* @param int|null $lotteryConfigId 要导入档位权重的奖池配置 ID;不传则使用记录中的 lottery_config_id(若有)
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function importFromRecord(int $recordId, ?int $lotteryConfigId = null): void
|
||||
{
|
||||
$record = $this->model->find($recordId);
|
||||
if (!$record) {
|
||||
throw new ApiException('测试记录不存在');
|
||||
}
|
||||
$record = is_array($record) ? $record : $record->toArray();
|
||||
|
||||
$snapshot = $record['weight_config_snapshot'] ?? null;
|
||||
if (is_string($snapshot)) {
|
||||
$snapshot = json_decode($snapshot, true);
|
||||
}
|
||||
if (is_array($snapshot) && !empty($snapshot)) {
|
||||
foreach ($snapshot as $item) {
|
||||
$id = isset($item['id']) ? (int) $item['id'] : 0;
|
||||
$weight = isset($item['weight']) ? (int) $item['weight'] : 1;
|
||||
if ($id > 0) {
|
||||
DiceRewardConfig::where('id', $id)->update(['weight' => $weight]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$tierSnapshot = $record['tier_weights_snapshot'] ?? null;
|
||||
if (is_string($tierSnapshot)) {
|
||||
$tierSnapshot = json_decode($tierSnapshot, true);
|
||||
}
|
||||
$targetLotteryId = $lotteryConfigId !== null && $lotteryConfigId > 0
|
||||
? $lotteryConfigId
|
||||
: (isset($record['lottery_config_id']) && (int) $record['lottery_config_id'] > 0 ? (int) $record['lottery_config_id'] : null);
|
||||
if (is_array($tierSnapshot) && !empty($tierSnapshot) && $targetLotteryId > 0) {
|
||||
$pool = DiceLotteryPoolConfig::find($targetLotteryId);
|
||||
if (!$pool) {
|
||||
throw new ApiException('奖池配置不存在');
|
||||
}
|
||||
$update = [
|
||||
't1_weight' => (int) ($tierSnapshot['T1'] ?? $tierSnapshot['t1'] ?? 0),
|
||||
't2_weight' => (int) ($tierSnapshot['T2'] ?? $tierSnapshot['t2'] ?? 0),
|
||||
't3_weight' => (int) ($tierSnapshot['T3'] ?? $tierSnapshot['t3'] ?? 0),
|
||||
't4_weight' => (int) ($tierSnapshot['T4'] ?? $tierSnapshot['t4'] ?? 0),
|
||||
't5_weight' => (int) ($tierSnapshot['T5'] ?? $tierSnapshot['t5'] ?? 0),
|
||||
];
|
||||
DiceLotteryPoolConfig::where('id', $targetLotteryId)->update($update);
|
||||
}
|
||||
|
||||
DiceRewardConfig::refreshCache();
|
||||
DiceRewardConfig::clearRequestInstance();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user