游戏-奖励配置-优化样式,新增创建指定表单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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user