2026-06-12 00:35:14 +08:00

662 lines
19 KiB
Go
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.

package gamemanagement
import (
"fmt"
"strings"
"unicode"
)
const (
robotNicknameLanguageArabic = "arabic"
robotNicknameLanguageEnglish = "english"
robotPresetTargetCount = 1000
defaultRobotGenerateCount = 20
maxRobotGenerateCount = 1000
)
var defaultRobotAvatarSources = []string{
"https://randomuser.me/api/portraits/men/0.jpg",
"https://randomuser.me/api/portraits/men/1.jpg",
"https://randomuser.me/api/portraits/men/2.jpg",
"https://randomuser.me/api/portraits/men/3.jpg",
"https://randomuser.me/api/portraits/men/4.jpg",
"https://randomuser.me/api/portraits/men/5.jpg",
"https://randomuser.me/api/portraits/men/6.jpg",
"https://randomuser.me/api/portraits/men/7.jpg",
"https://randomuser.me/api/portraits/men/8.jpg",
"https://randomuser.me/api/portraits/men/9.jpg",
"https://randomuser.me/api/portraits/men/10.jpg",
"https://randomuser.me/api/portraits/men/11.jpg",
"https://randomuser.me/api/portraits/men/12.jpg",
"https://randomuser.me/api/portraits/men/13.jpg",
"https://randomuser.me/api/portraits/men/14.jpg",
"https://randomuser.me/api/portraits/men/15.jpg",
"https://randomuser.me/api/portraits/men/16.jpg",
"https://randomuser.me/api/portraits/men/17.jpg",
"https://randomuser.me/api/portraits/men/18.jpg",
"https://randomuser.me/api/portraits/men/19.jpg",
"https://randomuser.me/api/portraits/men/20.jpg",
"https://randomuser.me/api/portraits/men/21.jpg",
"https://randomuser.me/api/portraits/men/22.jpg",
"https://randomuser.me/api/portraits/men/23.jpg",
"https://randomuser.me/api/portraits/men/24.jpg",
"https://randomuser.me/api/portraits/men/25.jpg",
"https://randomuser.me/api/portraits/men/26.jpg",
"https://randomuser.me/api/portraits/men/27.jpg",
"https://randomuser.me/api/portraits/men/28.jpg",
"https://randomuser.me/api/portraits/men/29.jpg",
"https://randomuser.me/api/portraits/men/30.jpg",
"https://randomuser.me/api/portraits/men/31.jpg",
"https://randomuser.me/api/portraits/men/32.jpg",
"https://randomuser.me/api/portraits/men/33.jpg",
"https://randomuser.me/api/portraits/men/34.jpg",
"https://randomuser.me/api/portraits/men/35.jpg",
"https://randomuser.me/api/portraits/men/36.jpg",
"https://randomuser.me/api/portraits/men/37.jpg",
"https://randomuser.me/api/portraits/men/38.jpg",
"https://randomuser.me/api/portraits/men/39.jpg",
"https://randomuser.me/api/portraits/men/40.jpg",
"https://randomuser.me/api/portraits/men/41.jpg",
"https://randomuser.me/api/portraits/men/42.jpg",
"https://randomuser.me/api/portraits/men/43.jpg",
"https://randomuser.me/api/portraits/men/44.jpg",
"https://randomuser.me/api/portraits/men/45.jpg",
"https://randomuser.me/api/portraits/men/46.jpg",
"https://randomuser.me/api/portraits/men/47.jpg",
"https://randomuser.me/api/portraits/men/48.jpg",
"https://randomuser.me/api/portraits/men/49.jpg",
"https://randomuser.me/api/portraits/men/50.jpg",
"https://randomuser.me/api/portraits/men/51.jpg",
"https://randomuser.me/api/portraits/men/52.jpg",
"https://randomuser.me/api/portraits/men/53.jpg",
"https://randomuser.me/api/portraits/men/54.jpg",
"https://randomuser.me/api/portraits/men/55.jpg",
"https://randomuser.me/api/portraits/men/56.jpg",
"https://randomuser.me/api/portraits/men/57.jpg",
"https://randomuser.me/api/portraits/men/58.jpg",
"https://randomuser.me/api/portraits/men/59.jpg",
"https://randomuser.me/api/portraits/men/60.jpg",
"https://randomuser.me/api/portraits/men/61.jpg",
"https://randomuser.me/api/portraits/men/62.jpg",
"https://randomuser.me/api/portraits/men/63.jpg",
"https://randomuser.me/api/portraits/men/64.jpg",
"https://randomuser.me/api/portraits/men/65.jpg",
"https://randomuser.me/api/portraits/men/66.jpg",
"https://randomuser.me/api/portraits/men/67.jpg",
"https://randomuser.me/api/portraits/men/68.jpg",
"https://randomuser.me/api/portraits/men/69.jpg",
"https://randomuser.me/api/portraits/men/70.jpg",
"https://randomuser.me/api/portraits/men/71.jpg",
"https://randomuser.me/api/portraits/men/72.jpg",
"https://randomuser.me/api/portraits/men/73.jpg",
"https://randomuser.me/api/portraits/men/74.jpg",
"https://randomuser.me/api/portraits/men/75.jpg",
"https://randomuser.me/api/portraits/men/76.jpg",
"https://randomuser.me/api/portraits/men/77.jpg",
"https://randomuser.me/api/portraits/men/78.jpg",
"https://randomuser.me/api/portraits/men/79.jpg",
"https://randomuser.me/api/portraits/men/80.jpg",
"https://randomuser.me/api/portraits/men/81.jpg",
"https://randomuser.me/api/portraits/men/82.jpg",
"https://randomuser.me/api/portraits/men/83.jpg",
"https://randomuser.me/api/portraits/men/84.jpg",
"https://randomuser.me/api/portraits/men/85.jpg",
"https://randomuser.me/api/portraits/men/86.jpg",
"https://randomuser.me/api/portraits/men/87.jpg",
"https://randomuser.me/api/portraits/men/88.jpg",
"https://randomuser.me/api/portraits/men/89.jpg",
"https://randomuser.me/api/portraits/men/90.jpg",
"https://randomuser.me/api/portraits/men/91.jpg",
"https://randomuser.me/api/portraits/men/92.jpg",
"https://randomuser.me/api/portraits/men/93.jpg",
"https://randomuser.me/api/portraits/men/94.jpg",
"https://randomuser.me/api/portraits/men/95.jpg",
"https://randomuser.me/api/portraits/men/96.jpg",
"https://randomuser.me/api/portraits/men/97.jpg",
"https://randomuser.me/api/portraits/men/98.jpg",
"https://randomuser.me/api/portraits/men/99.jpg",
"https://randomuser.me/api/portraits/women/0.jpg",
"https://randomuser.me/api/portraits/women/1.jpg",
"https://randomuser.me/api/portraits/women/2.jpg",
"https://randomuser.me/api/portraits/women/3.jpg",
"https://randomuser.me/api/portraits/women/4.jpg",
"https://randomuser.me/api/portraits/women/5.jpg",
"https://randomuser.me/api/portraits/women/6.jpg",
"https://randomuser.me/api/portraits/women/7.jpg",
"https://randomuser.me/api/portraits/women/8.jpg",
"https://randomuser.me/api/portraits/women/9.jpg",
"https://randomuser.me/api/portraits/women/10.jpg",
"https://randomuser.me/api/portraits/women/11.jpg",
"https://randomuser.me/api/portraits/women/12.jpg",
"https://randomuser.me/api/portraits/women/13.jpg",
"https://randomuser.me/api/portraits/women/14.jpg",
"https://randomuser.me/api/portraits/women/15.jpg",
"https://randomuser.me/api/portraits/women/16.jpg",
"https://randomuser.me/api/portraits/women/17.jpg",
"https://randomuser.me/api/portraits/women/18.jpg",
"https://randomuser.me/api/portraits/women/19.jpg",
"https://randomuser.me/api/portraits/women/20.jpg",
"https://randomuser.me/api/portraits/women/21.jpg",
"https://randomuser.me/api/portraits/women/22.jpg",
"https://randomuser.me/api/portraits/women/23.jpg",
"https://randomuser.me/api/portraits/women/24.jpg",
"https://randomuser.me/api/portraits/women/25.jpg",
"https://randomuser.me/api/portraits/women/26.jpg",
"https://randomuser.me/api/portraits/women/27.jpg",
"https://randomuser.me/api/portraits/women/28.jpg",
"https://randomuser.me/api/portraits/women/29.jpg",
"https://randomuser.me/api/portraits/women/30.jpg",
"https://randomuser.me/api/portraits/women/31.jpg",
"https://randomuser.me/api/portraits/women/32.jpg",
"https://randomuser.me/api/portraits/women/33.jpg",
"https://randomuser.me/api/portraits/women/34.jpg",
"https://randomuser.me/api/portraits/women/35.jpg",
"https://randomuser.me/api/portraits/women/36.jpg",
"https://randomuser.me/api/portraits/women/37.jpg",
"https://randomuser.me/api/portraits/women/38.jpg",
"https://randomuser.me/api/portraits/women/39.jpg",
"https://randomuser.me/api/portraits/women/40.jpg",
"https://randomuser.me/api/portraits/women/41.jpg",
"https://randomuser.me/api/portraits/women/42.jpg",
"https://randomuser.me/api/portraits/women/43.jpg",
"https://randomuser.me/api/portraits/women/44.jpg",
"https://randomuser.me/api/portraits/women/45.jpg",
"https://randomuser.me/api/portraits/women/46.jpg",
"https://randomuser.me/api/portraits/women/47.jpg",
"https://randomuser.me/api/portraits/women/48.jpg",
"https://randomuser.me/api/portraits/women/49.jpg",
"https://randomuser.me/api/portraits/women/50.jpg",
"https://randomuser.me/api/portraits/women/51.jpg",
"https://randomuser.me/api/portraits/women/52.jpg",
"https://randomuser.me/api/portraits/women/53.jpg",
"https://randomuser.me/api/portraits/women/54.jpg",
"https://randomuser.me/api/portraits/women/55.jpg",
"https://randomuser.me/api/portraits/women/56.jpg",
"https://randomuser.me/api/portraits/women/57.jpg",
"https://randomuser.me/api/portraits/women/58.jpg",
"https://randomuser.me/api/portraits/women/59.jpg",
"https://randomuser.me/api/portraits/women/60.jpg",
"https://randomuser.me/api/portraits/women/61.jpg",
"https://randomuser.me/api/portraits/women/62.jpg",
"https://randomuser.me/api/portraits/women/63.jpg",
"https://randomuser.me/api/portraits/women/64.jpg",
"https://randomuser.me/api/portraits/women/65.jpg",
"https://randomuser.me/api/portraits/women/66.jpg",
"https://randomuser.me/api/portraits/women/67.jpg",
"https://randomuser.me/api/portraits/women/68.jpg",
"https://randomuser.me/api/portraits/women/69.jpg",
"https://randomuser.me/api/portraits/women/70.jpg",
"https://randomuser.me/api/portraits/women/71.jpg",
"https://randomuser.me/api/portraits/women/72.jpg",
"https://randomuser.me/api/portraits/women/73.jpg",
"https://randomuser.me/api/portraits/women/74.jpg",
"https://randomuser.me/api/portraits/women/75.jpg",
"https://randomuser.me/api/portraits/women/76.jpg",
"https://randomuser.me/api/portraits/women/77.jpg",
"https://randomuser.me/api/portraits/women/78.jpg",
"https://randomuser.me/api/portraits/women/79.jpg",
"https://randomuser.me/api/portraits/women/80.jpg",
"https://randomuser.me/api/portraits/women/81.jpg",
"https://randomuser.me/api/portraits/women/82.jpg",
"https://randomuser.me/api/portraits/women/83.jpg",
"https://randomuser.me/api/portraits/women/84.jpg",
"https://randomuser.me/api/portraits/women/85.jpg",
"https://randomuser.me/api/portraits/women/86.jpg",
"https://randomuser.me/api/portraits/women/87.jpg",
"https://randomuser.me/api/portraits/women/88.jpg",
"https://randomuser.me/api/portraits/women/89.jpg",
"https://randomuser.me/api/portraits/women/90.jpg",
"https://randomuser.me/api/portraits/women/91.jpg",
"https://randomuser.me/api/portraits/women/92.jpg",
"https://randomuser.me/api/portraits/women/93.jpg",
"https://randomuser.me/api/portraits/women/94.jpg",
"https://randomuser.me/api/portraits/women/95.jpg",
"https://randomuser.me/api/portraits/women/96.jpg",
"https://randomuser.me/api/portraits/women/97.jpg",
"https://randomuser.me/api/portraits/women/98.jpg",
"https://randomuser.me/api/portraits/women/99.jpg",
}
var defaultArabicRobotNicknames = []string{
"أمير",
"سيف",
"مالك",
"فارس",
"ليث",
"بدر",
"ماهر",
"رامي",
"كريم",
"سامي",
"عمر",
"يوسف",
"آدم",
"ريان",
"زيد",
"نور",
"ياسين",
"أنس",
"خالد",
"طارق",
"حازم",
"نادر",
"وسيم",
"جواد",
"مروان",
"إياد",
"نبيل",
"هلال",
"تميم",
"سلمان",
"راشد",
"جاسم",
"ماجد",
"سلطان",
"عادل",
"هاني",
"بلال",
"يزن",
"غيث",
"قصي",
"سليم",
"ريان_الليل",
"نجم",
"ملك_اللعبة",
"صقر",
"ذيب",
"أسد",
"الزعيم",
"القناص",
"الأسطورة",
"ظل",
"برق",
"رعد",
"موج",
"نمر",
"شهاب",
"فهد",
"نايف",
"نوف",
"ليان",
"ريم",
"سارة",
"مريم",
"لينا",
"هنا",
"جود",
"تالا",
"دانا",
"لارا",
"ياسمين",
"نوران",
"رؤى",
"سما",
"ملك",
"جنى",
"آية",
"ندى",
"فرح",
"أمل",
"هدى",
"سلمى",
"ليلى",
"رنا",
"نورا",
"ديما",
"مايا",
"ميس",
"رهف",
"شهد",
"غلا",
"لولو",
"وردة",
"قمر",
"نجمة",
"لؤلؤة",
"الملكة",
"أميرة",
"روح",
"حلم",
"بسمة",
}
var defaultEnglishRobotNicknames = []string{
"Alex",
"Jordan",
"Taylor",
"Morgan",
"Riley",
"Nova",
"Phoenix",
"Hunter",
"Luna",
"Mia",
"Noah",
"Liam",
"Ava",
"Emma",
"Oliver",
"Elijah",
"James",
"William",
"Benjamin",
"Lucas",
"Mason",
"Ethan",
"Logan",
"Jackson",
"Levi",
"Sebastian",
"Mateo",
"Jack",
"Owen",
"Theodore",
"Aiden",
"Samuel",
"Joseph",
"John",
"David",
"Wyatt",
"Matthew",
"Luke",
"Asher",
"Carter",
"Julian",
"Grayson",
"Leo",
"Jayden",
"Gabriel",
"Isaac",
"Lincoln",
"Anthony",
"Hudson",
"Dylan",
"Ezra",
"Thomas",
"Charles",
"Christopher",
"Jaxon",
"Maverick",
"Josiah",
"Isaiah",
"Andrew",
"Elias",
"Joshua",
"Nathan",
"Caleb",
"Ryan",
"Adrian",
"Miles",
"Eli",
"Nolan",
"Christian",
"Aaron",
"Cameron",
"Ezekiel",
"Colton",
"Luca",
"Landon",
"Hannah",
"Sofia",
"Isabella",
"Amelia",
"Olivia",
"Charlotte",
"Harper",
"Evelyn",
"Abigail",
"Ella",
"Scarlett",
"Grace",
"Chloe",
"Victoria",
"Aria",
"Penelope",
"Layla",
"Zoey",
"Nora",
"Lily",
"Ellie",
"Violet",
"Aurora",
"Stella",
"Hazel",
}
var defaultEnglishRobotNicknameSuffixes = []string{
"River",
"Sky",
"Star",
"Moon",
"Storm",
"Stone",
"Vale",
"Lake",
"Light",
"Wave",
"Cloud",
"Flame",
"Forest",
"Dream",
"Shadow",
"Bloom",
"Quest",
"Spark",
"North",
"South",
"East",
"West",
"Dawn",
"Dusk",
"Peak",
}
var defaultArabicRobotNicknameSuffixes = []string{
"نور",
"قمر",
"نجم",
"سيف",
"بحر",
"مجد",
"حلم",
"روح",
"ورد",
"برق",
"رعد",
"شمس",
"ليل",
"فجر",
"درر",
"موج",
"فرح",
"أمل",
"غيم",
"ندى",
"سحر",
"لؤلؤ",
"ذهب",
"عز",
"وفا",
}
func init() {
defaultRobotAvatarSources = expandRobotAvatarSources(defaultRobotAvatarSources, robotPresetTargetCount)
defaultEnglishRobotNicknames = expandRobotNicknamePool(defaultEnglishRobotNicknames, defaultEnglishRobotNicknameSuffixes, robotPresetTargetCount, robotNicknameLanguageEnglish)
defaultArabicRobotNicknames = expandRobotNicknamePool(defaultArabicRobotNicknames, defaultArabicRobotNicknameSuffixes, robotPresetTargetCount, robotNicknameLanguageArabic)
}
func expandRobotAvatarSources(base []string, target int) []string {
// 头像池必须是固定顺序,不能运行时随机;后台批量生成 1000 个时按 index 取值,重复请求也能得到同一批源图。
if target <= 0 {
return nil
}
result := make([]string, 0, target)
seen := make(map[string]struct{}, target)
add := func(source string) bool {
source = strings.TrimSpace(source)
if source == "" {
return len(result) >= target
}
if _, ok := seen[source]; ok {
return len(result) >= target
}
seen[source] = struct{}{}
result = append(result, source)
return len(result) >= target
}
for _, source := range base {
if add(source) {
return result
}
}
for index := 1; len(result) < target; index++ {
// DiceBear 用 seed 生成确定性头像URL 唯一且返回 image/png这里只存源 URL真正头像仍会先转存到后台 COS。
add(fmt.Sprintf("https://api.dicebear.com/9.x/thumbs/png?seed=hyapp-robot-%04d", index))
}
return result
}
func expandRobotNicknamePool(base []string, suffixes []string, target int, language string) []string {
// 名称池也保持固定顺序:先保留已有预设名,再用同语言后缀组合扩容,避免数字后缀和英阿混排进入用户资料。
if target <= 0 {
return nil
}
result := make([]string, 0, target)
seen := make(map[string]struct{}, target)
add := func(name string) bool {
name = sanitizeRobotNickname(name, language)
if name == "" {
return len(result) >= target
}
if _, ok := seen[name]; ok {
return len(result) >= target
}
seen[name] = struct{}{}
result = append(result, name)
return len(result) >= target
}
for _, name := range base {
if add(name) {
return result
}
}
cleanBase := append([]string(nil), result...)
cleanSuffixes := make([]string, 0, len(suffixes))
for _, suffix := range suffixes {
suffix = sanitizeRobotNickname(suffix, language)
if suffix != "" {
cleanSuffixes = append(cleanSuffixes, suffix)
}
}
for _, name := range cleanBase {
for _, suffix := range cleanSuffixes {
if add(name + suffix) {
return result
}
}
}
for offset := 1; len(result) < target; offset++ {
for index, name := range cleanBase {
next := cleanBase[(index+offset)%len(cleanBase)]
if add(name + next) {
return result
}
}
}
return result
}
func normalizeRobotNicknameLanguage(value string) string {
// 后台只开放英语和阿拉伯语两类昵称池;未知值回退英语,避免历史请求继续落到英阿混合池。
switch strings.ToLower(strings.TrimSpace(value)) {
case "ar", "arabic", "arab":
return robotNicknameLanguageArabic
case "en", "english":
return robotNicknameLanguageEnglish
default:
return robotNicknameLanguageEnglish
}
}
func robotAccountLanguage(nicknameLanguage string) string {
// 用户资料里的 language 跟昵称池保持一致,后续客户端或运营筛选机器人时不用再反推昵称字符集。
if normalizeRobotNicknameLanguage(nicknameLanguage) == robotNicknameLanguageArabic {
return "ar"
}
return "en"
}
func robotNickname(index int, nicknameLanguage string) string {
// 机器人昵称只从单一语言池取值,不能再拼随机 hex否则会出现数字、下划线和英阿混排。
language := normalizeRobotNicknameLanguage(nicknameLanguage)
if language == robotNicknameLanguageArabic {
return robotNicknameFromPool(index, defaultArabicRobotNicknames, "لاعب", language)
}
return robotNicknameFromPool(index, defaultEnglishRobotNicknames, "Player", language)
}
func robotNicknameFromPool(index int, pool []string, fallback string, language string) string {
// 前 1000 个直接使用固定预设池;超过池大小时仍用同语言两个预设名拼接,保证手工超界调用也不带数字后缀。
if index < 0 {
index = 0
}
if len(pool) == 0 {
return fallback
}
base := sanitizeRobotNickname(pool[index%len(pool)], language)
if base == "" {
base = fallback
}
if index < len(pool) {
return base
}
next := sanitizeRobotNickname(pool[(index%len(pool)+1)%len(pool)], language)
if next == "" || next == base {
next = sanitizeRobotNickname(pool[(index+index/len(pool)+2)%len(pool)], language)
}
if next == "" || next == base {
return base
}
return base + next
}
func sanitizeRobotNickname(value string, language string) string {
// 预设里可能有旧的分隔符或非目标语言字符;写库前统一剔除,保证“英语纯英语、阿拉伯语纯阿拉伯语”。
var builder strings.Builder
for _, item := range strings.TrimSpace(value) {
switch language {
case robotNicknameLanguageArabic:
if unicode.Is(unicode.Arabic, item) {
builder.WriteRune(item)
}
default:
if (item >= 'A' && item <= 'Z') || (item >= 'a' && item <= 'z') {
builder.WriteRune(item)
}
}
}
return builder.String()
}
func robotGender(index int, requested string) string {
requested = strings.TrimSpace(requested)
if requested != "" {
return requested
}
if index%2 == 0 {
return "male"
}
return "female"
}
func robotAvatarSource(index int, requested []string) string {
if source := pickString(requested, index); source != "" {
return source
}
if len(defaultRobotAvatarSources) == 0 {
return ""
}
return defaultRobotAvatarSources[index%len(defaultRobotAvatarSources)]
}