游戏-奖励配置-优化样式,新增创建指定表单btn,新增创建奖励权重配置btn
This commit is contained in:
273
app/common/library/GameRewardTierBoardGenerator.php
Normal file
273
app/common/library/GameRewardTierBoardGenerator.php
Normal 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));
|
||||
}
|
||||
}
|
||||
172
app/common/library/GameRewardWeightSeeder.php
Normal file
172
app/common/library/GameRewardWeightSeeder.php
Normal 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('档位奖励表单点数须在 5~30');
|
||||
}
|
||||
$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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user