游戏-奖励配置-优化样式,新增创建指定表单btn,新增创建奖励权重配置btn

This commit is contained in:
2026-04-13 16:27:25 +08:00
parent 6c94d03ddf
commit c9d21d8216
8 changed files with 1112 additions and 23 deletions

View File

@@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
namespace app\common\library;
/**
* 按「顺/逆时针摇点落格」条数约束生成 26 格档位盘面(写入 tier_reward_form JSON
*/
final class GameRewardTierBoardGenerator
{
private const LEOPARD = [5, 10, 15, 20, 25, 30];
private static function landingCw(int $d): int
{
$start = $d - 5;
return ($start + $d) % 26;
}
/** 图二:逆时针 end = start D若小于 0 则 26 + start D */
private static function landingCcw(int $d): int
{
$start = $d - 5;
$x = $start - $d;
return $x >= 0 ? $x : 26 + $start - $d;
}
/**
* 豹子点数:该次摇取顺、逆落点档位不能为 T4、T5
*
* @param list<string> $tier
*/
private static function leopardOk(array $tier): bool
{
foreach (self::LEOPARD as $d) {
foreach ([self::landingCw($d), self::landingCcw($d)] as $idx) {
$t = $tier[$idx];
if ($t === 'T4' || $t === 'T5') {
return false;
}
}
}
return true;
}
/**
* @param array<string, mixed> $params
* @return array{tier_reward_form: string}
*/
public static function generate(array $params): array
{
$t1Cw = self::intParam($params, 't1_fixed_cw');
$t1Ccw = self::intParam($params, 't1_fixed_ccw');
$t2MinCw = self::intParam($params, 't2_min_cw');
$t2MinCcw = self::intParam($params, 't2_min_ccw');
$t4Cw = self::intParam($params, 't4_fixed_cw');
$t4Ccw = self::intParam($params, 't4_fixed_ccw');
$t5Cw = self::intParam($params, 't5_fixed_cw');
$t5Ccw = self::intParam($params, 't5_fixed_ccw');
$amt1 = self::numParam($params, 'amt_t1');
$amt2 = self::numParam($params, 'amt_t2');
$amt3 = self::numParam($params, 'amt_t3');
$amt4 = self::numParam($params, 'amt_t4');
$bestTier = null;
$bestScore = INF;
for ($attempt = 0; $attempt < 32; $attempt++) {
$tier = self::randomInitialTier($attempt);
$temp = 5.0;
for ($step = 0; $step < 8000; $step++) {
$score = self::score($tier, $t1Cw, $t1Ccw, $t2MinCw, $t2MinCcw, $t4Cw, $t4Ccw, $t5Cw, $t5Ccw);
if ($score < $bestScore) {
$bestScore = $score;
$bestTier = $tier;
}
$i = mt_rand(0, 25);
$j = mt_rand(0, 25);
if ($i === $j) {
continue;
}
$oldI = $tier[$i];
$oldJ = $tier[$j];
$tier[$i] = $oldJ;
$tier[$j] = $oldI;
if (!self::leopardOk($tier)) {
$tier[$i] = $oldI;
$tier[$j] = $oldJ;
continue;
}
$newScore = self::score($tier, $t1Cw, $t1Ccw, $t2MinCw, $t2MinCcw, $t4Cw, $t4Ccw, $t5Cw, $t5Ccw);
$delta = $newScore - $score;
$u = mt_rand() / max(1, mt_getrandmax());
if ($delta < 0 || ($temp > 0.02 && exp(-$delta / $temp) > $u)) {
// keep
} else {
$tier[$i] = $oldI;
$tier[$j] = $oldJ;
}
$temp *= 0.999;
}
}
if ($bestTier === null || $bestScore > 45) {
throw new \RuntimeException('无法在豹子与条数约束下收敛盘面,请调整条数后重试');
}
$json = self::buildJson($bestTier, $amt1, $amt2, $amt3, $amt4);
return ['tier_reward_form' => $json];
}
/** @return list<string> */
private static function randomInitialTier(int $seedBias): array
{
mt_srand((int) (microtime(true) * 1000000) + $seedBias * 10007);
$tier = [];
for ($p = 0; $p < 26; $p++) {
$tier[$p] = ['T1', 'T2', 'T3'][mt_rand(0, 2)];
}
if (!self::leopardOk($tier)) {
for ($p = 0; $p < 26; $p++) {
$tier[$p] = 'T3';
}
}
return $tier;
}
/**
* @param list<string> $tier
*/
private static function score(
array $tier,
int $t1Cw,
int $t1Ccw,
int $t2MinCw,
int $t2MinCcw,
int $t4Cw,
int $t4Ccw,
int $t5Cw,
int $t5Ccw
): float {
if (!self::leopardOk($tier)) {
return 1e9;
}
$h = self::histogram($tier);
$s = 0.0;
$s += ($h['cw']['T1'] - $t1Cw) ** 2;
$s += ($h['ccw']['T1'] - $t1Ccw) ** 2;
$s += max(0, $t2MinCw - $h['cw']['T2']) ** 2 * 8;
$s += max(0, $t2MinCcw - $h['ccw']['T2']) ** 2 * 8;
$s += ($h['cw']['T4'] - $t4Cw) ** 2;
$s += ($h['ccw']['T4'] - $t4Ccw) ** 2;
$s += ($h['cw']['T5'] - $t5Cw) ** 2;
$s += ($h['ccw']['T5'] - $t5Ccw) ** 2;
return $s;
}
/**
* @param list<string> $tier
* @return array{cw: array<string, int>, ccw: array<string, int>}
*/
private static function histogram(array $tier): array
{
$cw = ['T1' => 0, 'T2' => 0, 'T3' => 0, 'T4' => 0, 'T5' => 0];
$ccw = ['T1' => 0, 'T2' => 0, 'T3' => 0, 'T4' => 0, 'T5' => 0];
for ($d = 5; $d <= 30; $d++) {
$cw[$tier[self::landingCw($d)]]++;
$ccw[$tier[self::landingCcw($d)]]++;
}
return ['cw' => $cw, 'ccw' => $ccw];
}
/**
* @param list<string> $tier
*/
private static function remarkForTier(string $t, float $amt2): string
{
if ($t === 'T1') {
return '大奖';
}
if ($t === 'T2') {
return $amt2 < 100 ? '完美回本' : '小赚';
}
if ($t === 'T3') {
return '抽水';
}
if ($t === 'T4') {
return '惩罚';
}
if ($t === 'T5') {
return '再来一次';
}
return '';
}
private static function buildJson(array $tier, float $amt1, float $amt2, float $amt3, float $amt4): string
{
$rows = [];
for ($i = 0; $i < 26; $i++) {
$t = $tier[$i];
if ($t === 'T5') {
$ui = '再来一次';
$uiEn = 'Once again';
$ev = '0';
} else {
$a = match ($t) {
'T1' => $amt1,
'T2' => $amt2,
'T3' => $amt3,
'T4' => $amt4,
default => $amt3,
};
$ui = self::fmtMoney($a);
$uiEn = self::fmtMoney($a);
$ev = self::fmtMoney($a);
}
$rows[] = [
'grid_number' => strval(5 + $i),
'ui_text' => $ui,
'ui_text_en' => $uiEn,
'real_ev' => $ev,
'tier' => $t,
'remark' => self::remarkForTier($t, $amt2),
];
}
return json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
}
private static function fmtMoney(float $v): string
{
if (abs($v - round($v)) < 0.000001) {
return strval((int) round($v));
}
return rtrim(rtrim(sprintf('%.4f', $v), '0'), '.');
}
private static function intParam(array $params, string $key): int
{
$v = $params[$key] ?? 0;
if (is_string($v) && trim($v) === '') {
return 0;
}
if (!is_numeric($v)) {
return 0;
}
return intval(strval($v));
}
private static function numParam(array $params, string $key): float
{
$v = $params[$key] ?? 0;
if (is_string($v) && trim($v) === '') {
return 0.0;
}
if (!is_numeric($v)) {
return 0.0;
}
return floatval(strval($v));
}
}

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace app\common\library;
use support\think\Db;
use Throwable;
/**
* 根据档位奖励 JSON 生成 game_reward_weight 对照(先删后插)
*/
final class GameRewardWeightSeeder
{
private const LEOPARD = [5, 10, 15, 20, 25, 30];
/**
* @throws Throwable
*/
public static function syncFromTierRewardForm(int $gameChannelId, string $tierRewardFormJson): void
{
$decoded = json_decode($tierRewardFormJson, true);
if (!is_array($decoded) || count($decoded) !== 26) {
throw new \RuntimeException('档位奖励表单必须为 26 条且为合法 JSON');
}
$byGrid = [];
foreach ($decoded as $idx => $row) {
if (!is_array($row)) {
throw new \RuntimeException('档位奖励表单第' . strval($idx + 1) . '条格式错误');
}
$g = $row['grid_number'] ?? null;
if (!is_numeric($g)) {
throw new \RuntimeException('档位奖励表单第' . strval($idx + 1) . '条点数无效');
}
$gi = intval(strval($g));
if ($gi < 5 || $gi > 30) {
throw new \RuntimeException('档位奖励表单点数须在 530');
}
$byGrid[$gi] = $row;
}
$cells = [];
for ($i = 0; $i < 26; $i++) {
$g = 5 + $i;
if (!isset($byGrid[$g])) {
throw new \RuntimeException('档位奖励表单缺少点数 ' . strval($g));
}
$cells[$i] = self::normalizeCell($byGrid[$g], $i + 1);
}
self::assertLeopardOk($cells);
$batch = [];
for ($d = 5; $d <= 30; $d++) {
$start = $d - 5;
$endCw = ($start + $d) % 26;
$x = $start - $d;
$endCcw = $x >= 0 ? $x : 26 + $start - $d;
$batch[] = self::buildInsertRow($gameChannelId, 0, $d, $start, $endCw, $cells[$endCw]);
$batch[] = self::buildInsertRow($gameChannelId, 1, $d, $start, $endCcw, $cells[$endCcw]);
}
$now = time();
foreach ($batch as $k => $_) {
$batch[$k]['create_time'] = $now;
$batch[$k]['update_time'] = $now;
}
Db::startTrans();
try {
Db::name('game_reward_weight')->where('game_channel_id', $gameChannelId)->delete();
Db::name('game_reward_weight')->insertAll($batch);
Db::commit();
} catch (Throwable $e) {
Db::rollback();
$msg = $e->getMessage();
if (str_contains($msg, 'game_reward_weight') || str_contains($msg, "doesn't exist")) {
throw new \RuntimeException('写入失败:请确认已创建数据表 game_reward_weight 并已执行迁移。' . $msg);
}
throw $e;
}
}
/**
* @param list<array{ui_text: string, real_ev: float, tier: string, remark: string}> $cells
*/
private static function assertLeopardOk(array $cells): void
{
foreach (self::LEOPARD as $d) {
$start = $d - 5;
$endCw = ($start + $d) % 26;
$x = $start - $d;
$endCcw = $x >= 0 ? $x : 26 + $start - $d;
foreach ([$endCw, $endCcw] as $idx) {
$t = $cells[$idx]['tier'];
if ($t === 'T4' || $t === 'T5') {
throw new \RuntimeException(
'豹子点数 ' . strval($d) . ' 的落点不能为 T4/T5请先在档位表中调整后再生成权重对照'
);
}
}
}
}
/**
* @param array<string, mixed> $row
* @return array{ui_text: string, real_ev: float, tier: string, remark: string}
*/
private static function normalizeCell(array $row, int $rowNo): array
{
$ui = $row['ui_text'] ?? null;
$ev = $row['real_ev'] ?? null;
$tier = $row['tier'] ?? null;
if (!is_string($ui) || trim($ui) === '') {
throw new \RuntimeException('档位奖励表单第' . strval($rowNo) . '条显示文本不能为空');
}
if ($ev === null || $ev === '' || !is_numeric($ev)) {
throw new \RuntimeException('档位奖励表单第' . strval($rowNo) . '条实际中奖无效');
}
if (!is_string($tier) || !in_array($tier, ['T1', 'T2', 'T3', 'T4', 'T5'], true)) {
throw new \RuntimeException('档位奖励表单第' . strval($rowNo) . '条档位无效');
}
$remark = $row['remark'] ?? '';
$remarkStr = is_string($remark) ? $remark : '';
return [
'ui_text' => $ui,
'real_ev' => floatval(strval($ev)),
'tier' => $tier,
'remark' => $remarkStr,
];
}
/**
* @param array{ui_text: string, real_ev: float, tier: string, remark: string} $cell
* @return array<string, int|float|string>
*/
private static function buildInsertRow(
int $gameChannelId,
int $direction,
int $gridNumber,
int $startIndex,
int $endIndex,
array $cell
): array {
return [
'game_channel_id' => $gameChannelId,
'direction' => $direction,
'grid_number' => $gridNumber,
'start_index' => $startIndex,
'end_index' => $endIndex,
'ui_text' => $cell['ui_text'],
'real_ev' => $cell['real_ev'],
'tier' => $cell['tier'],
'type' => self::tierToType($cell['tier']),
'remark' => $cell['remark'],
'weight' => 1,
];
}
private static function tierToType(string $tier): int
{
return match ($tier) {
'T1' => 3,
'T2' => 2,
'T3' => -1,
'T4' => -2,
'T5' => 1,
};
}
}