#!/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()