Files
moonwell-core/tools/generate-playerbots-ru-names.py

315 lines
12 KiB
Python
Raw Permalink 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.
#!/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()