227 lines
8.2 KiB
PHP
227 lines
8.2 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\library\game;
|
||
|
||
use InvalidArgumentException;
|
||
|
||
/**
|
||
* 36 字花字典(game_config.zi_hua_36_dictionary):仅存 JSON 数组
|
||
* category 使用英文:zodiac|beast|fowl|vermin|divine
|
||
*/
|
||
final class ZiHuaDictionary
|
||
{
|
||
public const CONFIG_KEY = 'zi_hua_36_dictionary';
|
||
|
||
/** 与表单下拉一致,顺序即展示顺序 */
|
||
public const CATEGORIES = ['zodiac', 'beast', 'fowl', 'vermin', 'divine'];
|
||
|
||
private const LEGACY_CATEGORY_MAP = [
|
||
'生肖' => 'zodiac',
|
||
'猛兽' => 'beast',
|
||
'飞禽' => 'fowl',
|
||
'虫蛇' => 'vermin',
|
||
'神兽' => 'divine',
|
||
'zodiac' => 'zodiac',
|
||
'beast' => 'beast',
|
||
'fowl' => 'fowl',
|
||
'vermin' => 'vermin',
|
||
'divine' => 'divine',
|
||
];
|
||
|
||
/**
|
||
* 默认 36 条(与 PRD 一致,category 为英文)
|
||
*
|
||
* @return list<array{no: int, name: string, category: string}>
|
||
*/
|
||
public static function defaultItems(): array
|
||
{
|
||
return [
|
||
['no' => 1, 'name' => '鼠', 'category' => 'zodiac'],
|
||
['no' => 2, 'name' => '牛', 'category' => 'zodiac'],
|
||
['no' => 3, 'name' => '虎', 'category' => 'zodiac'],
|
||
['no' => 4, 'name' => '兔', 'category' => 'zodiac'],
|
||
['no' => 5, 'name' => '龙', 'category' => 'zodiac'],
|
||
['no' => 6, 'name' => '蛇', 'category' => 'zodiac'],
|
||
['no' => 7, 'name' => '马', 'category' => 'zodiac'],
|
||
['no' => 8, 'name' => '羊', 'category' => 'zodiac'],
|
||
['no' => 9, 'name' => '猴', 'category' => 'zodiac'],
|
||
['no' => 10, 'name' => '鸡', 'category' => 'zodiac'],
|
||
['no' => 11, 'name' => '狗', 'category' => 'zodiac'],
|
||
['no' => 12, 'name' => '猪', 'category' => 'zodiac'],
|
||
['no' => 13, 'name' => '狮', 'category' => 'beast'],
|
||
['no' => 14, 'name' => '象', 'category' => 'beast'],
|
||
['no' => 15, 'name' => '鹿', 'category' => 'beast'],
|
||
['no' => 16, 'name' => '熊', 'category' => 'beast'],
|
||
['no' => 17, 'name' => '狼', 'category' => 'beast'],
|
||
['no' => 18, 'name' => '豹', 'category' => 'beast'],
|
||
['no' => 19, 'name' => '鹰', 'category' => 'fowl'],
|
||
['no' => 20, 'name' => '鹤', 'category' => 'fowl'],
|
||
['no' => 21, 'name' => '孔雀', 'category' => 'fowl'],
|
||
['no' => 22, 'name' => '鸳鸯', 'category' => 'fowl'],
|
||
['no' => 23, 'name' => '鸵鸟', 'category' => 'fowl'],
|
||
['no' => 24, 'name' => '天鹅', 'category' => 'fowl'],
|
||
['no' => 25, 'name' => '蟾蜍', 'category' => 'vermin'],
|
||
['no' => 26, 'name' => '蜘蛛', 'category' => 'vermin'],
|
||
['no' => 27, 'name' => '蝙蝠', 'category' => 'vermin'],
|
||
['no' => 28, 'name' => '蜈蚣', 'category' => 'vermin'],
|
||
['no' => 29, 'name' => '蝎', 'category' => 'vermin'],
|
||
['no' => 30, 'name' => '蛇蜥', 'category' => 'vermin'],
|
||
['no' => 31, 'name' => '麒麟', 'category' => 'divine'],
|
||
['no' => 32, 'name' => '凤凰', 'category' => 'divine'],
|
||
['no' => 33, 'name' => '青龙', 'category' => 'divine'],
|
||
['no' => 34, 'name' => '白虎', 'category' => 'divine'],
|
||
['no' => 35, 'name' => '朱雀', 'category' => 'divine'],
|
||
['no' => 36, 'name' => '玄武', 'category' => 'divine'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @return list<array{no: int, name: string, category: string}>
|
||
*/
|
||
public static function parseFromConfigValue(?string $configValue): array
|
||
{
|
||
if ($configValue === null || trim($configValue) === '') {
|
||
return self::defaultItems();
|
||
}
|
||
$decoded = json_decode($configValue, true);
|
||
if (!is_array($decoded)) {
|
||
return self::defaultItems();
|
||
}
|
||
if (isset($decoded['items']) && is_array($decoded['items'])) {
|
||
return self::normalizeItemsList($decoded['items']);
|
||
}
|
||
if ($decoded !== [] && array_is_list($decoded)) {
|
||
return self::normalizeItemsList($decoded);
|
||
}
|
||
return self::defaultItems();
|
||
}
|
||
|
||
/**
|
||
* @param list<mixed> $items
|
||
* @return list<array{no: int, name: string, category: string}>
|
||
*/
|
||
public static function normalizeItemsList(array $items): array
|
||
{
|
||
$defaults = self::defaultItems();
|
||
$defaultByNo = [];
|
||
foreach ($defaults as $row) {
|
||
$defaultByNo[$row['no']] = $row;
|
||
}
|
||
|
||
$out = [];
|
||
foreach ($items as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$no = $row['no'] ?? null;
|
||
if (!is_numeric($no)) {
|
||
continue;
|
||
}
|
||
$n = intval($no, 10);
|
||
if ($n < 1 || $n > 36) {
|
||
continue;
|
||
}
|
||
$name = isset($row['name']) && is_string($row['name']) ? trim($row['name']) : '';
|
||
if ($name === '') {
|
||
$name = $defaultByNo[$n]['name'] ?? '';
|
||
}
|
||
$catRaw = $row['category'] ?? 'zodiac';
|
||
$catStr = is_string($catRaw) ? trim($catRaw) : '';
|
||
$category = self::LEGACY_CATEGORY_MAP[$catStr] ?? (in_array($catStr, self::CATEGORIES, true) ? $catStr : 'zodiac');
|
||
$out[] = ['no' => $n, 'name' => $name, 'category' => $category];
|
||
}
|
||
|
||
if (count($out) !== 36) {
|
||
return self::defaultItems();
|
||
}
|
||
|
||
usort($out, static fn (array $a, array $b): int => $a['no'] <=> $b['no']);
|
||
|
||
$seen = [];
|
||
foreach ($out as $row) {
|
||
if (isset($seen[$row['no']])) {
|
||
return self::defaultItems();
|
||
}
|
||
$seen[$row['no']] = true;
|
||
}
|
||
|
||
for ($i = 1; $i <= 36; $i++) {
|
||
if (!isset($seen[$i])) {
|
||
return self::defaultItems();
|
||
}
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 校验 POST 数据并输出入库用的 36 条(no 升序)
|
||
*
|
||
* @param list<array{no?: mixed, name?: mixed, category?: mixed}> $items
|
||
*
|
||
* @return list<array{no: int, name: string, category: string}>
|
||
*
|
||
* @throws InvalidArgumentException
|
||
*/
|
||
public static function prepareItemsForSave(array $items): array
|
||
{
|
||
if (count($items) !== 36) {
|
||
throw new InvalidArgumentException('Must be exactly 36 items');
|
||
}
|
||
$nos = [];
|
||
$out = [];
|
||
foreach ($items as $idx => $row) {
|
||
if (!is_array($row)) {
|
||
throw new InvalidArgumentException('Row format error');
|
||
}
|
||
$no = $row['no'] ?? null;
|
||
if (!is_numeric($no)) {
|
||
throw new InvalidArgumentException('No must be numeric');
|
||
}
|
||
$n = intval($no, 10);
|
||
if ($n < 1 || $n > 36) {
|
||
throw new InvalidArgumentException('No must be between 1 and 36');
|
||
}
|
||
if (isset($nos[$n])) {
|
||
throw new InvalidArgumentException('Duplicate no');
|
||
}
|
||
$nos[$n] = true;
|
||
|
||
$name = $row['name'] ?? '';
|
||
if (!is_string($name) || trim($name) === '') {
|
||
throw new InvalidArgumentException('Name is required');
|
||
}
|
||
|
||
$cat = $row['category'] ?? '';
|
||
$catTrim = is_string($cat) ? trim($cat) : '';
|
||
if (!in_array($catTrim, self::CATEGORIES, true)) {
|
||
throw new InvalidArgumentException('Category is invalid');
|
||
}
|
||
|
||
$out[] = ['no' => $n, 'name' => trim($name), 'category' => $catTrim];
|
||
}
|
||
for ($i = 1; $i <= 36; $i++) {
|
||
if (!isset($nos[$i])) {
|
||
throw new InvalidArgumentException('Missing no');
|
||
}
|
||
}
|
||
usort($out, static fn (array $a, array $b): int => $a['no'] <=> $b['no']);
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* @param list<array{no: int, name: string, category: string}> $items
|
||
*/
|
||
public static function encodeForDb(array $items): string
|
||
{
|
||
usort($items, static fn (array $a, array $b): int => $a['no'] <=> $b['no']);
|
||
$encoded = json_encode($items, JSON_UNESCAPED_UNICODE);
|
||
if ($encoded === false) {
|
||
throw new InvalidArgumentException('JSON encode failed');
|
||
}
|
||
return $encoded;
|
||
}
|
||
}
|