боты русифицированы
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Generate deterministic Cyrillic playerbot character and guild name pools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUTPUT = (
|
||||
ROOT
|
||||
/ "modules/mod-playerbots/data/sql/characters/updates/"
|
||||
/ "2026_07_30_00_playerbots_russian_names.sql"
|
||||
)
|
||||
NAMES_PER_CATEGORY = 250
|
||||
UPSTREAM_NAMES = (
|
||||
ROOT
|
||||
/ "modules/mod-playerbots/data/sql/characters/base/playerbots_names.sql"
|
||||
)
|
||||
|
||||
|
||||
CATEGORIES = {
|
||||
0: (
|
||||
["Ал", "Бор", "Вел", "Влад", "Град", "Дар", "Мир", "Рад", "Свят", "Яр"],
|
||||
["", "о", "е", "и", "а"],
|
||||
["ан", "ен", "ий", "ор", "слав", "мир", "дан", "бор", "гор", "вит"],
|
||||
),
|
||||
1: (
|
||||
["Аль", "Бож", "Вес", "Дар", "Злат", "Лад", "Люб", "Мил", "Рад", "Яр"],
|
||||
["", "о", "е", "и", "а"],
|
||||
["ана", "ена", "ина", "ира", "исса", "лава", "мира", "яна", "ея", "ора"],
|
||||
),
|
||||
2: (
|
||||
["Блик", "Винт", "Гайк", "Жуж", "Искр", "Клап", "Порш", "Рыч", "Тик", "Шпун"],
|
||||
["", "о", "е", "и", "а"],
|
||||
["ик", "ак", "ун", "ил", "ер", "орт", "ен", "ос", "ин", "ей"],
|
||||
),
|
||||
3: (
|
||||
["Блик", "Винт", "Гайк", "Жуж", "Искр", "Кноп", "Пруж", "Тик", "Шпул", "Шестер"],
|
||||
["", "о", "е", "и", "а"],
|
||||
["ика", "ана", "инка", "елла", "ира", "ина", "етта", "исса", "уна", "ея"],
|
||||
),
|
||||
4: (
|
||||
["Бран", "Бром", "Гим", "Дор", "Каз", "Краг", "Мор", "Тор", "Фар", "Хар"],
|
||||
["", "о", "е", "и", "а"],
|
||||
["дин", "гар", "грим", "мунд", "бор", "рик", "вар", "драм", "гран", "ор"],
|
||||
),
|
||||
5: (
|
||||
["Бран", "Бром", "Гим", "Дор", "Каз", "Краг", "Мор", "Тор", "Фар", "Хель"],
|
||||
["", "о", "е", "и", "а"],
|
||||
["дина", "гара", "гримма", "мунда", "бора", "рика", "вара", "дра", "грана", "ина"],
|
||||
),
|
||||
6: (
|
||||
["Аэл", "Илл", "Лиар", "Мэл", "Ним", "Саэл", "Тал", "Фаэр", "Эли", "Яв"],
|
||||
["", "о", "е", "и", "а"],
|
||||
["дор", "рион", "лас", "тир", "ниэл", "вен", "рон", "лир", "тис", "эль"],
|
||||
),
|
||||
7: (
|
||||
["Аэл", "Илл", "Лиа", "Мэл", "Ниа", "Саэл", "Тали", "Фаэ", "Эли", "Яви"],
|
||||
["", "о", "е", "и", "а"],
|
||||
["дора", "риэль", "ласса", "тира", "ниэль", "вена", "рона", "лира", "тисса", "эля"],
|
||||
),
|
||||
8: (
|
||||
["Ака", "Ару", "Ваа", "Ири", "Каа", "Наэ", "Оро", "Таа", "Эре", "Яна"],
|
||||
["", "л", "р", "н", "м"],
|
||||
["дор", "ниус", "мар", "рион", "тар", "вен", "мон", "дар", "рос", "эль"],
|
||||
),
|
||||
9: (
|
||||
["Ака", "Ару", "Ваа", "Ири", "Каа", "Наэ", "Оро", "Таа", "Эре", "Яна"],
|
||||
["", "л", "р", "н", "м"],
|
||||
["дора", "ния", "мара", "риэль", "тара", "вена", "мона", "дара", "роса", "эла"],
|
||||
),
|
||||
10: (
|
||||
["Гар", "Гром", "Драк", "Карг", "Мок", "Рок", "Тар", "Ург", "Хар", "Зуг"],
|
||||
["", "о", "а", "у", "и"],
|
||||
["аш", "мак", "гар", "док", "рат", "гул", "нак", "рок", "тар", "зог"],
|
||||
),
|
||||
11: (
|
||||
["Гра", "Гром", "Дра", "Кар", "Мо", "Ро", "Тар", "Ур", "Хар", "Зу"],
|
||||
["", "о", "а", "у", "и"],
|
||||
["ша", "мака", "гара", "дока", "рата", "гула", "нака", "рока", "тара", "зога"],
|
||||
),
|
||||
12: (
|
||||
["Вол", "Джин", "Зан", "Зул", "Каз", "Раз", "Сэн", "Таз", "Хак", "Ям"],
|
||||
["", "а", "о", "и", "у"],
|
||||
["джи", "зар", "кан", "мон", "рак", "тал", "хан", "зин", "джо", "мар"],
|
||||
),
|
||||
13: (
|
||||
["Вола", "Джи", "Зана", "Зула", "Каза", "Раза", "Сэна", "Таза", "Хака", "Яма"],
|
||||
["", "а", "о", "и", "у"],
|
||||
["джи", "зара", "кана", "мона", "рака", "тала", "хана", "зина", "джа", "мара"],
|
||||
),
|
||||
14: (
|
||||
["Гром", "Кам", "Коп", "Неб", "Рог", "Степ", "Туч", "Шрам", "Ветр", "Биз"],
|
||||
["", "о", "е", "и", "а"],
|
||||
["орог", "оступ", "ебык", "огрив", "окоп", "оступ", "ешаг", "арог", "овет", "он"],
|
||||
),
|
||||
15: (
|
||||
["Гром", "Кам", "Коп", "Неб", "Рог", "Степ", "Туч", "Шрам", "Ветр", "Биз"],
|
||||
["", "о", "е", "и", "а"],
|
||||
["орогая", "оступа", "ебыка", "огрива", "окопа", "ешага", "арога", "овета", "она", "ани"],
|
||||
),
|
||||
16: (
|
||||
["Алер", "Вей", "Каэл", "Кел", "Лор", "Сал", "Тал", "Фен", "Эль", "Эрен"],
|
||||
["", "а", "е", "и", "о"],
|
||||
["ан", "ион", "дир", "лас", "рен", "тос", "вар", "лен", "рис", "эль"],
|
||||
),
|
||||
17: (
|
||||
["Але", "Вей", "Каэ", "Кели", "Лори", "Сали", "Тали", "Фе", "Эли", "Эре"],
|
||||
["", "а", "е", "и", "о"],
|
||||
["ана", "иэль", "дира", "ласса", "рена", "тоса", "вара", "лена", "риса", "эля"],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
GUILD_PREFIXES = [
|
||||
"Стражи", "Клинки", "Герои", "Хранители", "Вестники", "Воины",
|
||||
"Дети", "Сыны", "Дочери", "Рыцари", "Искатели", "Защитники",
|
||||
]
|
||||
GUILD_OBJECTS = [
|
||||
"Рассвета", "Заката", "Севера", "Юга", "Азерота", "Калимдора",
|
||||
"Бури", "Пламени", "Льда", "Теней", "Луны", "Солнца",
|
||||
"Чести", "Славы", "Свободы", "Надежды", "Дракона", "Ворона",
|
||||
]
|
||||
GUILD_ADJECTIVES = [
|
||||
"Лунный", "Стальной", "Серебряный", "Золотой", "Алый", "Сумрачный",
|
||||
"Ночной", "Северный", "Вечный", "Тихий", "Грозовой", "Огненный",
|
||||
]
|
||||
GUILD_NOUNS = [
|
||||
"Дозор", "Легион", "Орден", "Союз", "Круг", "Щит",
|
||||
"Клинок", "Рубеж", "Рассвет", "Оплот", "Поход", "Ветер",
|
||||
]
|
||||
|
||||
|
||||
def valid_name(value: str) -> bool:
|
||||
return (
|
||||
2 <= len(value) <= 12
|
||||
and value.isalpha()
|
||||
and all("А" <= char <= "я" or char in "Ёё" for char in value)
|
||||
and not any(value[index] == value[index - 1] == value[index - 2] for index in range(2, len(value)))
|
||||
)
|
||||
|
||||
|
||||
TRANSLITERATION = {
|
||||
"shch": "щ",
|
||||
"sch": "щ",
|
||||
"zh": "ж",
|
||||
"kh": "х",
|
||||
"ts": "ц",
|
||||
"ch": "ч",
|
||||
"sh": "ш",
|
||||
"th": "т",
|
||||
"ph": "ф",
|
||||
"qu": "кв",
|
||||
"ck": "к",
|
||||
"ya": "я",
|
||||
"yo": "ё",
|
||||
"yu": "ю",
|
||||
"a": "а",
|
||||
"b": "б",
|
||||
"c": "к",
|
||||
"d": "д",
|
||||
"e": "е",
|
||||
"f": "ф",
|
||||
"g": "г",
|
||||
"h": "х",
|
||||
"i": "и",
|
||||
"j": "дж",
|
||||
"k": "к",
|
||||
"l": "л",
|
||||
"m": "м",
|
||||
"n": "н",
|
||||
"o": "о",
|
||||
"p": "п",
|
||||
"q": "к",
|
||||
"r": "р",
|
||||
"s": "с",
|
||||
"t": "т",
|
||||
"u": "у",
|
||||
"v": "в",
|
||||
"w": "в",
|
||||
"x": "кс",
|
||||
"y": "й",
|
||||
"z": "з",
|
||||
}
|
||||
|
||||
|
||||
def transliterate_name(value: str) -> str:
|
||||
source = value.casefold()
|
||||
translated: list[str] = []
|
||||
index = 0
|
||||
keys = sorted(TRANSLITERATION, key=len, reverse=True)
|
||||
|
||||
while index < len(source):
|
||||
for key in keys:
|
||||
if source.startswith(key, index):
|
||||
translated.append(TRANSLITERATION[key])
|
||||
index += len(key)
|
||||
break
|
||||
else:
|
||||
return ""
|
||||
|
||||
return "".join(translated).capitalize()
|
||||
|
||||
|
||||
def mysql_unicode_ci_key(value: str) -> str:
|
||||
"""Approximate the equivalences used by the target utf8mb4_unicode_ci index."""
|
||||
return value.casefold().replace("ё", "е").replace("й", "и")
|
||||
|
||||
|
||||
def generate_character_names() -> list[tuple[str, int]]:
|
||||
used: set[str] = set()
|
||||
result: list[tuple[str, int]] = []
|
||||
by_category: dict[int, list[str]] = {category: [] for category in range(18)}
|
||||
row_pattern = re.compile(r"^\(\d+,'([^']+)',(\d+)\)[,;]$")
|
||||
|
||||
for line in UPSTREAM_NAMES.read_text(encoding="utf-8").splitlines():
|
||||
match = row_pattern.match(line.strip())
|
||||
if not match:
|
||||
continue
|
||||
|
||||
original, category_text = match.groups()
|
||||
category = int(category_text)
|
||||
if category not in by_category or len(by_category[category]) >= NAMES_PER_CATEGORY:
|
||||
continue
|
||||
|
||||
candidate = transliterate_name(original)
|
||||
candidate_key = mysql_unicode_ci_key(candidate)
|
||||
if not valid_name(candidate) or candidate_key in used:
|
||||
continue
|
||||
|
||||
used.add(candidate_key)
|
||||
by_category[category].append(candidate)
|
||||
|
||||
for category, category_names in by_category.items():
|
||||
if len(category_names) != NAMES_PER_CATEGORY:
|
||||
raise RuntimeError(
|
||||
f"Category {category} produced {len(category_names)} names, expected {NAMES_PER_CATEGORY}"
|
||||
)
|
||||
result.extend((name, category) for name in category_names)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_guild_names() -> list[str]:
|
||||
candidates = [
|
||||
f"{prefix} {obj}"
|
||||
for prefix, obj in itertools.product(GUILD_PREFIXES, GUILD_OBJECTS)
|
||||
]
|
||||
candidates.extend(
|
||||
f"{adjective} {noun}"
|
||||
for adjective, noun in itertools.product(GUILD_ADJECTIVES, GUILD_NOUNS)
|
||||
)
|
||||
return list(dict.fromkeys(name for name in candidates if len(name) <= 24))
|
||||
|
||||
|
||||
def sql_quote(value: str) -> str:
|
||||
return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
names = generate_character_names()
|
||||
guild_names = generate_guild_names()
|
||||
|
||||
lines = [
|
||||
"-- MoonWell static Cyrillic name pools for mod-playerbots.",
|
||||
"-- Generated by tools/generate-playerbots-ru-names.py.",
|
||||
"SET NAMES utf8mb4;",
|
||||
"",
|
||||
"CREATE TABLE IF NOT EXISTS `playerbots_names_ru` (",
|
||||
" `name_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,",
|
||||
" `name` VARCHAR(24) NOT NULL,",
|
||||
" `gender` TINYINT UNSIGNED NOT NULL,",
|
||||
" PRIMARY KEY (`name_id`),",
|
||||
" KEY `idx_playerbots_names_ru_name` (`name`)",
|
||||
") ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;",
|
||||
"",
|
||||
"TRUNCATE TABLE `playerbots_names_ru`;",
|
||||
"INSERT INTO `playerbots_names_ru` (`name`, `gender`) VALUES",
|
||||
]
|
||||
lines.extend(
|
||||
f"({sql_quote(name)}, {category}){',' if index + 1 < len(names) else ';'}"
|
||||
for index, (name, category) in enumerate(names)
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"CREATE TABLE IF NOT EXISTS `playerbots_guild_names_ru` (",
|
||||
" `name_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,",
|
||||
" `name` VARCHAR(24) NOT NULL,",
|
||||
" PRIMARY KEY (`name_id`),",
|
||||
" UNIQUE KEY `uq_playerbots_guild_names_ru_name` (`name`)",
|
||||
") ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;",
|
||||
"",
|
||||
"TRUNCATE TABLE `playerbots_guild_names_ru`;",
|
||||
"INSERT INTO `playerbots_guild_names_ru` (`name`) VALUES",
|
||||
]
|
||||
)
|
||||
lines.extend(
|
||||
f"({sql_quote(name)}){',' if index + 1 < len(guild_names) else ';'}"
|
||||
for index, name in enumerate(guild_names)
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT.write_text("\n".join(lines), encoding="utf-8")
|
||||
print(f"Wrote {len(names)} character names and {len(guild_names)} guild names to {OUTPUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user