[色子游戏]奖励配置权重测试记录

This commit is contained in:
2026-03-11 18:12:19 +08:00
parent 2af7fedcce
commit 064ce06393
18 changed files with 1720 additions and 19 deletions

View File

@@ -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 奖池配置IDDiceLotteryPoolConfig用于设定 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];
}
}

View File

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