Files
webman-buildadmin/app/common/library/GameRewardTierBoardGenerator.php

274 lines
7.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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));
}
}