一键测试权重-新增安全线杀分机制,保证测试数据合理
This commit is contained in:
@@ -56,8 +56,11 @@
|
||||
"weightTest": {
|
||||
"title": "One-Click Weight Test",
|
||||
"alertTitle": "Bonus pool logic",
|
||||
"alertBody": "Same as playStart draw: uses name=default safety line and kill switch; when profit is below the line, paid tickets use player tier weights (custom below), free tickets use killScore; when profit reaches the line and kill is on, both use killScore.",
|
||||
"alertBody": "Test mode is non-kill by default. You can enable kill mode below with switch + safety line: once simulated player cumulative profit reaches the line, paid draws switch to killScore.",
|
||||
"chainModeHint": "Simulation: set paid spin counts only (CW/CCW). If a paid draw hits “play again” (or T5), the next draw is free with the same ante, lottery type free, paid amount 0. Free-draw tier odds are configured below (including chained free plays).",
|
||||
"killModeHint": "When test kill mode is enabled: use simulated player cumulative profit as trigger; once cumulative profit >= safety line, subsequent paid draws use killScore. Free draws still follow the configured free settings.",
|
||||
"labelKillModeEnabled": "Enable test kill mode",
|
||||
"labelTestSafetyLine": "Test safety line",
|
||||
"sectionPaid": "Paid draws",
|
||||
"sectionFreeAfterPlayAgain": "Free draw tier odds (after play-again)",
|
||||
"tierProbHintFreeChain": "When using custom tier odds: T1–T5 below apply when a free draw runs (tier roll; combined with dice_reward row weights).",
|
||||
@@ -80,6 +83,7 @@
|
||||
"btnCancel": "Cancel",
|
||||
"warnAnte": "Ante must be greater than 0",
|
||||
"warnPaidSpins": "Paid clockwise + counter-clockwise spin counts must be greater than 0",
|
||||
"warnTestSafetyLine": "Test safety line must be greater than or equal to 0",
|
||||
"warnTotalSpins": "At least one of paid/free direction spin counts must be greater than 0",
|
||||
"warnPaidTierSumPositive": "When no paid pool is selected, T1–T5 odds sum must be greater than 0",
|
||||
"warnPaidTierSumMax": "Paid T1–T5 odds sum cannot exceed 100%",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"toolbar": {
|
||||
"viewDetail": "View Detail"
|
||||
},
|
||||
"search": {
|
||||
"paidPlannedSpins": "Planned paid spins",
|
||||
"ante": "Ante"
|
||||
},
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"clockwiseAbbr": "CW",
|
||||
@@ -12,6 +16,7 @@
|
||||
"chainModeYes": "Yes",
|
||||
"chainModeNo": "No",
|
||||
"paidPlannedSpins": "Planned paid spins",
|
||||
"ante": "Ante",
|
||||
"playAgainCount": "Play-again count",
|
||||
"progressDraws": "{over} done",
|
||||
"progressFailed": "{over} before fail",
|
||||
|
||||
@@ -56,8 +56,11 @@
|
||||
"weightTest": {
|
||||
"title": "一键测试权重",
|
||||
"alertTitle": "彩金池逻辑说明",
|
||||
"alertBody": "与 playStart 抽奖逻辑一致:使用 name=default 的安全线、杀分开关;盈利未达安全线时,付费抽奖券使用玩家自身权重(下方自定义档位),免费抽奖券使用 killScore 配置;盈利达到安全线且杀分开启时,付费/免费均使用 killScore 配置。",
|
||||
"alertBody": "测试模式默认不启用杀分切换;可通过下方“杀分开关 + 安全线”在测试内启用:当模拟玩家累计盈利达到安全线后,付费抽奖切换到 killScore。",
|
||||
"chainModeHint": "模拟方式:只配置付费抽奖次数(顺/逆时针)。付费抽到「再来一次」或 T5 时,下一局自动为免费抽奖,底注与触发局相同,抽奖类型记为免费、付费金额记为 0。免费抽奖的档位概率由下方「免费抽奖」配置决定(含通过再来一次触发的后续免费局)。",
|
||||
"killModeHint": "杀分开关开启后:以“模拟玩家累计盈利”作为判定值;当累计盈利 >= 安全线时,后续付费抽奖按 killScore 配置抽取;免费抽奖仍按“免费抽奖配置”执行。",
|
||||
"labelKillModeEnabled": "开启测试内杀分",
|
||||
"labelTestSafetyLine": "测试安全线",
|
||||
"sectionPaid": "付费抽奖",
|
||||
"sectionFreeAfterPlayAgain": "免费抽奖(再来一次后的档位概率)",
|
||||
"tierProbHintFreeChain": "当使用自定义档位时:以下为「免费抽奖」时 T1~T5 的档位概率(仅在有免费局时参与摇档,与 dice_reward 格子权重共同决定结果)。",
|
||||
@@ -80,6 +83,7 @@
|
||||
"btnCancel": "取消",
|
||||
"warnAnte": "底注 ante 必须大于 0",
|
||||
"warnPaidSpins": "付费抽奖顺时针与逆时针次数之和须大于 0",
|
||||
"warnTestSafetyLine": "测试安全线必须大于或等于 0",
|
||||
"warnTotalSpins": "付费或免费至少一种方向次数之和大于 0",
|
||||
"warnPaidTierSumPositive": "付费未选奖池时,T1~T5 档位概率之和需大于 0",
|
||||
"warnPaidTierSumMax": "付费档位概率 T1~T5 之和不能超过 100%",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"toolbar": {
|
||||
"viewDetail": "查看详情"
|
||||
},
|
||||
"search": {
|
||||
"paidPlannedSpins": "计划付费次数",
|
||||
"ante": "底注"
|
||||
},
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"clockwiseAbbr": "顺",
|
||||
@@ -12,6 +16,7 @@
|
||||
"chainModeYes": "是",
|
||||
"chainModeNo": "否",
|
||||
"paidPlannedSpins": "计划付费次数",
|
||||
"ante": "底注",
|
||||
"playAgainCount": "再来一次次数",
|
||||
"progressDraws": "已完成 {over} 次",
|
||||
"progressFailed": "失败前 {over} 次",
|
||||
|
||||
@@ -66,6 +66,8 @@ export default {
|
||||
paid_lottery_config_id?: number
|
||||
free_lottery_config_id?: number
|
||||
chain_free_mode?: boolean
|
||||
kill_mode_enabled?: boolean
|
||||
test_safety_line?: number
|
||||
s_count?: number
|
||||
n_count?: number
|
||||
paid_s_count?: number
|
||||
|
||||
@@ -14,10 +14,25 @@
|
||||
<ElAlert type="warning" :closable="false" show-icon class="weight-test-tip chain-tip">
|
||||
{{ $t('page.weightTest.chainModeHint') }}
|
||||
</ElAlert>
|
||||
<ElAlert type="info" :closable="false" show-icon class="weight-test-tip chain-tip">
|
||||
{{ $t('page.weightTest.killModeHint') }}
|
||||
</ElAlert>
|
||||
<ElForm :model="form" label-width="140px">
|
||||
<ElFormItem :label="$t('page.weightTest.labelAnte')" prop="ante" required>
|
||||
<ElInputNumber v-model="form.ante" :min="1" :step="1" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('page.weightTest.labelKillModeEnabled')" prop="kill_mode_enabled">
|
||||
<ElSwitch v-model="form.kill_mode_enabled" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('page.weightTest.labelTestSafetyLine')" prop="test_safety_line">
|
||||
<ElInputNumber
|
||||
v-model="form.test_safety_line"
|
||||
:min="0"
|
||||
:step="100"
|
||||
:disabled="!form.kill_mode_enabled"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<div class="section-title">{{ $t('page.weightTest.sectionPaid') }}</div>
|
||||
<ElFormItem
|
||||
@@ -164,7 +179,9 @@
|
||||
paid_tier_weights: { T1: 20, T2: 20, T3: 20, T4: 20, T5: 20 } as Record<string, number>,
|
||||
free_tier_weights: { T1: 20, T2: 20, T3: 20, T4: 20, T5: 20 } as Record<string, number>,
|
||||
paid_s_count: 100,
|
||||
paid_n_count: 100
|
||||
paid_n_count: 100,
|
||||
kill_mode_enabled: false,
|
||||
test_safety_line: 5000
|
||||
})
|
||||
const lotteryOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
/** 付费抽奖券可选档位:name=default */
|
||||
@@ -242,7 +259,9 @@
|
||||
paid_n_count: form.paid_n_count,
|
||||
free_s_count: 0,
|
||||
free_n_count: 0,
|
||||
chain_free_mode: true
|
||||
chain_free_mode: true,
|
||||
kill_mode_enabled: form.kill_mode_enabled,
|
||||
test_safety_line: form.test_safety_line
|
||||
}
|
||||
if (form.paid_lottery_config_id != null) {
|
||||
payload.paid_lottery_config_id = form.paid_lottery_config_id
|
||||
@@ -266,6 +285,10 @@
|
||||
ElMessage.warning(t('page.weightTest.warnPaidSpins'))
|
||||
return false
|
||||
}
|
||||
if (form.kill_mode_enabled && (form.test_safety_line == null || form.test_safety_line < 0)) {
|
||||
ElMessage.warning(t('page.weightTest.warnTestSafetyLine'))
|
||||
return false
|
||||
}
|
||||
const needPaidTier = form.paid_lottery_config_id == null
|
||||
const needFreeTier = form.free_lottery_config_id == null
|
||||
if (needPaidTier) {
|
||||
|
||||
@@ -225,6 +225,12 @@
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
prop: 'ante',
|
||||
label: 'page.table.ante',
|
||||
width: 90,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
prop: 'play_again_count',
|
||||
label: 'page.table.playAgainCount',
|
||||
|
||||
@@ -8,6 +8,32 @@
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item :label="$t('page.search.paidPlannedSpins')" prop="paid_planned_spins">
|
||||
<el-input-number
|
||||
v-model="formData.paid_planned_spins"
|
||||
:placeholder="$t('table.searchBar.all')"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item :label="$t('page.search.ante')" prop="ante">
|
||||
<el-input-number
|
||||
v-model="formData.ante"
|
||||
:placeholder="$t('table.searchBar.all')"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ class DiceRewardController extends BaseController
|
||||
* 参数:lottery_config_id 可选;paid_tier_weights / free_tier_weights 自定义档位;
|
||||
* paid_s_count, paid_n_count
|
||||
* chain_free_mode=1:仅按付费次数模拟;付费抽到再来一次/T5 则在队列中插入免费局(同底注、lottery_type=免费、paid_amount=0)
|
||||
* kill_mode_enabled=1:测试内启用杀分;当模拟玩家累计盈利达到 test_safety_line 后,付费抽奖切到 killScore
|
||||
*/
|
||||
#[Permission('一键测试权重', 'dice:reward:index:startWeightTest')]
|
||||
public function startWeightTest(Request $request): Response
|
||||
@@ -100,6 +101,8 @@ class DiceRewardController extends BaseController
|
||||
'paid_tier_weights' => $post['paid_tier_weights'] ?? null,
|
||||
'free_tier_weights' => $post['free_tier_weights'] ?? null,
|
||||
'chain_free_mode' => $post['chain_free_mode'] ?? null,
|
||||
'kill_mode_enabled' => $post['kill_mode_enabled'] ?? null,
|
||||
'test_safety_line' => $post['test_safety_line'] ?? null,
|
||||
];
|
||||
$adminId = isset($this->adminInfo['id']) ? (int) $this->adminInfo['id'] : null;
|
||||
try {
|
||||
|
||||
@@ -38,6 +38,8 @@ class DiceRewardConfigRecordController extends BaseController
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$where = $request->more([
|
||||
['paid_planned_spins', ''],
|
||||
['ante', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
$data = $this->logic->getList($query);
|
||||
|
||||
@@ -270,6 +270,11 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
$paidS = isset($params['paid_s_count']) ? (int) $params['paid_s_count'] : 0;
|
||||
$paidN = isset($params['paid_n_count']) ? (int) $params['paid_n_count'] : 0;
|
||||
$chainFreeMode = !empty($params['chain_free_mode']);
|
||||
$killModeEnabled = !empty($params['kill_mode_enabled']);
|
||||
$testSafetyLine = isset($params['test_safety_line']) ? (int) $params['test_safety_line'] : 5000;
|
||||
if ($testSafetyLine < 0) {
|
||||
throw new ApiException('test_safety_line must be greater than or equal to 0');
|
||||
}
|
||||
|
||||
foreach ([$paidS, $paidN] as $c) {
|
||||
if ($c !== 0 && !in_array($c, $allowed, true)) {
|
||||
@@ -398,6 +403,8 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
$record = new DiceRewardConfigRecord();
|
||||
$plannedPaidSpins = $paidS + $paidN;
|
||||
$record->chain_free_mode = $chainFreeMode ? 1 : 0;
|
||||
$record->kill_mode_enabled = $killModeEnabled ? 1 : 0;
|
||||
$record->test_safety_line = $testSafetyLine;
|
||||
$record->paid_planned_spins = $plannedPaidSpins;
|
||||
// 总抽奖次数与 test_count 仅在任务成功结束时写入(见 WeightTestRunner::markSuccess)
|
||||
$record->test_count = 0;
|
||||
|
||||
@@ -14,7 +14,7 @@ use support\think\Db;
|
||||
|
||||
/**
|
||||
* 一键测试权重:单进程后台执行模拟摇色子,写入 dice_play_record_test 并更新 dice_reward_config_record 进度
|
||||
* 抽奖逻辑与 PlayStartLogic 一致:使用 name=default 的安全线、杀分开关;盈利<安全线时付费用玩家权重、免费用 killScore;盈利>=安全线且杀分开启时付费/免费均用 killScore
|
||||
* 支持测试内杀分:当模拟玩家累计盈利达到安全线后,付费抽奖切换到 killScore
|
||||
*/
|
||||
class WeightTestRunner
|
||||
{
|
||||
@@ -41,8 +41,7 @@ class WeightTestRunner
|
||||
];
|
||||
|
||||
/**
|
||||
* 执行指定测试记录:按付费/免费、顺/逆方向交替模拟(付费顺→付费逆→免费顺→免费逆),每 10 条写入一次测试表并更新进度
|
||||
* 使用与 playStart 相同的彩金池逻辑:name=default 的安全线/kill_enabled;付费用 paid_tier_weights(玩家权重)或 killScore;免费用 killScore
|
||||
* 执行指定测试记录:按付费次数模拟,若命中 T5 则链式插入免费局(同方向同底注)
|
||||
* @param int $recordId dice_reward_config_record.id
|
||||
*/
|
||||
public function run(int $recordId): void
|
||||
@@ -68,8 +67,6 @@ class WeightTestRunner
|
||||
$this->markFailed($recordId, '彩金池配置 name=default 不存在');
|
||||
return;
|
||||
}
|
||||
$safetyLine = (int) ($configType0->safety_line ?? 0);
|
||||
$killEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1;
|
||||
|
||||
$paidTierWeightsCustom = (is_array($record->paid_tier_weights ?? null) && $record->paid_tier_weights !== [])
|
||||
? $record->paid_tier_weights
|
||||
@@ -103,8 +100,14 @@ class WeightTestRunner
|
||||
DiceRewardConfig::clearRequestInstance();
|
||||
DiceReward::clearRequestInstance();
|
||||
|
||||
// 彩金池累计盈利:用于判断是否触发杀分(不再依赖单个玩家累计盈利)
|
||||
$poolProfitTotal = floatval($configType0->profit_amount ?? 0);
|
||||
$killModeEnabled = (int) ($record->kill_mode_enabled ?? 0) === 1;
|
||||
$testSafetyLine = (int) ($record->test_safety_line ?? 5000);
|
||||
if ($testSafetyLine < 0) {
|
||||
$testSafetyLine = 0;
|
||||
}
|
||||
|
||||
// 测试内“玩家累计盈利”:用于控制付费局是否切换杀分
|
||||
$playerProfitTotal = 0.0;
|
||||
|
||||
$playLogic = new PlayStartLogic();
|
||||
$resultCounts = [];
|
||||
@@ -121,13 +124,12 @@ class WeightTestRunner
|
||||
$ante,
|
||||
$paidPoolConfig,
|
||||
$freePoolConfig,
|
||||
$configType1,
|
||||
$paidTierWeightsCustom,
|
||||
$freeTierWeightsCustom,
|
||||
$configType0,
|
||||
$configType1,
|
||||
$safetyLine,
|
||||
$killEnabled,
|
||||
$poolProfitTotal,
|
||||
$killModeEnabled,
|
||||
$testSafetyLine,
|
||||
$playerProfitTotal,
|
||||
$resultCounts,
|
||||
$tierCounts,
|
||||
$buffer,
|
||||
@@ -157,13 +159,12 @@ class WeightTestRunner
|
||||
int $ante,
|
||||
$paidPoolConfig,
|
||||
$freePoolConfig,
|
||||
$killPoolConfig,
|
||||
?array $paidTierWeightsCustom,
|
||||
?array $freeTierWeightsCustom,
|
||||
$configType0,
|
||||
$configType1,
|
||||
int $safetyLine,
|
||||
bool $killEnabled,
|
||||
float &$poolProfitTotal,
|
||||
bool $killModeEnabled,
|
||||
int $testSafetyLine,
|
||||
float &$playerProfitTotal,
|
||||
array &$resultCounts,
|
||||
array &$tierCounts,
|
||||
array &$buffer,
|
||||
@@ -185,17 +186,23 @@ class WeightTestRunner
|
||||
$lotteryType = $isPaid ? 0 : 1;
|
||||
|
||||
if ($isPaid) {
|
||||
$usePoolWeights = $killEnabled && $poolProfitTotal >= $safetyLine && $configType1 !== null;
|
||||
$cfg = $usePoolWeights ? $configType1 : $paidPoolConfig;
|
||||
$customWeights = $usePoolWeights ? null : $paidTierWeightsCustom;
|
||||
$useKillForPaid = $killModeEnabled && $playerProfitTotal >= $testSafetyLine && $killPoolConfig !== null;
|
||||
if ($useKillForPaid) {
|
||||
$cfg = $killPoolConfig;
|
||||
$customWeights = null;
|
||||
} else {
|
||||
$cfg = $paidPoolConfig;
|
||||
$customWeights = $paidTierWeightsCustom;
|
||||
}
|
||||
} else {
|
||||
$useKillMode = $killEnabled && $poolProfitTotal >= $safetyLine && $configType1 !== null;
|
||||
$cfg = $useKillMode ? $configType1 : $freePoolConfig;
|
||||
$customWeights = $useKillMode ? null : $freeTierWeightsCustom;
|
||||
$cfg = $freePoolConfig;
|
||||
$customWeights = $freeTierWeightsCustom;
|
||||
}
|
||||
|
||||
$row = $playLogic->simulateOnePlay($cfg, $dir, $lotteryType, $playAnte, $customWeights);
|
||||
$this->accumulateProfitForDefault($row, $lotteryType, $cfg, $configType0, $poolProfitTotal);
|
||||
$winCoin = (float) ($row['win_coin'] ?? 0);
|
||||
$paidAmount = (float) ($row['paid_amount'] ?? 0);
|
||||
$playerProfitTotal += $winCoin - $paidAmount;
|
||||
$this->aggregate($row, $resultCounts, $tierCounts);
|
||||
$buffer[] = $this->rowForInsert($row, $recordId);
|
||||
$done++;
|
||||
@@ -210,23 +217,6 @@ class WeightTestRunner
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 累加彩金池累计盈利,用于触发杀分,与 PlayStartLogic 一致
|
||||
* @param int $lotteryType 0=付费券,1=免费券
|
||||
* @param object $usedConfig 本次使用的奖池配置(仅用于校验非空)
|
||||
* @param object $configType0 name=default 的彩金池
|
||||
* @param float $playerProfitTotal 实际为“彩金池累计盈利”滚动值
|
||||
*/
|
||||
private function accumulateProfitForDefault(array $row, int $lotteryType, $usedConfig, $configType0, float &$playerProfitTotal): void
|
||||
{
|
||||
if (($lotteryType !== 0 && $lotteryType !== 1) || $usedConfig === null || $configType0 === null || !isset($row['win_coin'])) {
|
||||
return;
|
||||
}
|
||||
$winCoin = (float) $row['win_coin'];
|
||||
$paidAmount = (float) ($row['paid_amount'] ?? 0);
|
||||
$playerProfitTotal += $lotteryType === 0 ? ($winCoin - $paidAmount) : $winCoin;
|
||||
}
|
||||
|
||||
private function aggregate(array $row, array &$resultCounts, array &$tierCounts): void
|
||||
{
|
||||
$grid = (int) ($row['roll_number_for_count'] ?? $row['roll_number'] ?? 0);
|
||||
|
||||
@@ -30,6 +30,8 @@ use think\model\relation\HasMany;
|
||||
* @property int $paid_s_count 付费抽奖顺时针次数
|
||||
* @property int $paid_n_count 付费抽奖逆时针次数
|
||||
* @property int $chain_free_mode 1=链式再来一次免费抽奖
|
||||
* @property int $kill_mode_enabled 测试内杀分开关 1=开启
|
||||
* @property int $test_safety_line 测试内安全线(模拟玩家累计盈利阈值)
|
||||
* @property int $paid_planned_spins 计划付费抽奖次数(顺+逆)
|
||||
* @property int $play_again_count 再来一次次数(T5触发次数)
|
||||
* @property array|null $paid_tier_weights 付费自定义档位权重 T1-T5
|
||||
@@ -68,6 +70,22 @@ class DiceRewardConfigRecord extends BaseModel
|
||||
return $this->hasMany(DicePlayRecordTest::class, 'reward_config_record_id', 'id');
|
||||
}
|
||||
|
||||
/** 计划付费抽奖次数(顺+逆) */
|
||||
public function searchPaidPlannedSpinsAttr($query, $value): void
|
||||
{
|
||||
if ($value !== '' && $value !== null) {
|
||||
$query->where('paid_planned_spins', '=', $value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 底注/注数(dice_ante_config.mult) */
|
||||
public function searchAnteAttr($query, $value): void
|
||||
{
|
||||
if ($value !== '' && $value !== null) {
|
||||
$query->where('ante', '=', $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联的 DicePlayRecordTest 统计平台赚取平台币
|
||||
* platform_profit = 关联的付费(lottery_type=0)付费金额求和(paid_amount) - 关联的 win_coin 求和
|
||||
|
||||
Reference in New Issue
Block a user