#!/usr/bin/env python3 """Генератор ников для ботов — порт nickgen.html (логика 1:1). Режимы: python nickgen.py --print 30 [--seed 42] напечатать N ников python nickgen.py [--seed 42] dry-run: показать, что изменится python nickgen.py --apply [--seed 42] переписать имена в сидах: server/game/data/FakePlayerProgression.xml () server/db_installer/sql/spp_clan_seed.sql (characters.char_name) Соответствие: character id=N в XML <-> charId 270001000+N в SQL, имена ставятся из одного списка, так что XML и БД остаются согласованы. После --apply ники в работающей БД не изменятся (INSERT IGNORE) — нужен reset-bots.ps1 и пересборка образа. """ import argparse import random import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent XML_PATH = ROOT / 'server' / 'game' / 'data' / 'FakePlayerProgression.xml' SQL_PATH = ROOT / 'server' / 'db_installer' / 'sql' / 'spp_clan_seed.sql' CHAR_ID_BASE = 270001000 # --- Пулы слов (как в nickgen.html) ------------------------------------------ RU_ADJ = ['злой', 'добрый', 'старый', 'дикий', 'мелкий', 'жирный', 'хитрый', 'бешеный', 'мамкин', 'просто', 'лысый', 'грозный', 'тихий', 'сонный', 'голодный', 'кривой', 'ржавый', 'бодрый', 'унылый', 'модный', 'четкий', 'дерзкий', 'борзый', 'лютый', 'шустрый', 'реальный', 'угарный', 'наглый'] RU_NOUN = ['гном', 'пес', 'кот', 'волк', 'сухарь', 'травка', 'пельмень', 'колбаса', 'мармелад', 'чума', 'ведьмак', 'сквозняк', 'холодок', 'бомж', 'топор', 'кабан', 'хомяк', 'огурец', 'батон', 'чеснок', 'валенок', 'тапок', 'кефир', 'компот', 'барсук', 'ежик', 'мухомор', 'лопата', 'кирпич', 'шашлык', 'беляш', 'танкист', 'дед', 'сосед', 'медоед', 'каратель', 'монах', 'гопник', 'чебурек', 'шаурма', 'семки', 'пацан', 'браток', 'кент', 'батя', 'гараж', 'подъезд', 'чушпан', 'задрот', 'нуб', 'читер', 'красавчик', 'нагибатор', 'убиватор', 'терминатор'] RU_NAME = ['олег', 'вася', 'дашка', 'ленка', 'федор', 'санек', 'толик', 'серега', 'витек', 'петруха', 'колян', 'димон', 'настя', 'машка', 'гриша', 'степан', 'юрец', 'макс', 'жека', 'вован', 'толян', 'саня', 'диман', 'леха', 'андрюха', 'кирюха', 'паша', 'стас', 'руслан', 'тимур', 'натаха', 'светка', 'танюха', 'ирка', 'вовчик', 'петрович', 'палыч'] RU_SURNAME = ['пупкин', 'сидоров', 'петров', 'иванов', 'смирнов', 'кузнецов', 'козлов', 'волков', 'зайцев', 'медведев'] PREFIXES = ['MC', 'Dj', 'Mr'] EN_ADJ = ['Dark', 'Shadow', 'Storm', 'Night', 'Blood', 'Ghost', 'Crazy', 'Old', 'Swift', 'Silent', 'Grim', 'Mad', 'Evil', 'Frost', 'Iron', 'Black', 'Wild', 'Lone', 'Sweet', 'True'] EN_NOUN = ['Slayer', 'Reaper', 'Hunter', 'Killer', 'Knight', 'Rogue', 'Monk', 'Rider', 'Arrow', 'Blade', 'Master', 'Lord', 'King', 'Sniper', 'Mage', 'Angel', 'Demon', 'Raven', 'Wolf', 'Dragon', 'Fire', 'Ice', 'Death', 'Star', 'Dream', 'Soul', 'Fang', 'Claw', 'Bishop', 'Titan'] # --- Транслит: кириллица -> латиница ----------------------------------------- TRANSLIT = {'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'e', 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch', 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya'} # --- Псевдокириллица: латинские двойники (KpucuK, TpaBka, IIeJIbMeHb) --------- # None = нет похожего символа, берётся транслит буквы. FAKE_CYR = {'а': 'a', 'б': '6', 'в': 'B', 'г': 'r', 'д': None, 'е': 'e', 'ё': 'e', 'ж': None, 'з': '3', 'и': 'u', 'й': 'u', 'к': 'K', 'л': 'JI', 'м': 'M', 'н': 'H', 'о': 'o', 'п': 'II', 'р': 'p', 'с': 'c', 'т': 'T', 'у': 'y', 'ф': None, 'х': 'x', 'ц': None, 'ч': '4', 'ш': 'LLI', 'щ': None, 'ъ': '', 'ы': 'bI', 'ь': 'b', 'э': '3', 'ю': 'IO', 'я': '9I'} # --- l33t для латинских слов (Ki4iga, OlegBI4) -------------------------------- LEET = {'e': '3', 'o': '0', 'a': '4', 'i': '1', 's': '5'} VALID = re.compile(r'^[A-Za-z0-9]{3,16}$') RU_RE = re.compile(r'[а-яё]') class NickGen: def __init__(self, seed=None, translit=True, fake_cyr=True, leet=True, digits=True, wrap=True, weights=(25, 55, 20)): self.r = random.Random(seed) self.translit = translit self.fake_cyr = fake_cyr self.leet = leet self.digits = digits self.wrap = wrap self.weights = weights def chance(self, pct): return self.r.randrange(100) < pct def pick(self, arr): return arr[self.r.randrange(len(arr))] def translit_word(self, w): out = '' for ch in w: t = TRANSLIT.get(ch, ch) if ch == 'й' and self.chance(50): t = 'y' if ch == 'в' and self.chance(15): t = 'w' # Wowan-стиль if ch == 'х': t = 'x' if self.chance(40) else ('kh' if self.chance(30) else 'h') # Xyligan-стиль out += t return out[:1].upper() + out[1:] def fake_cyr_word(self, w): out = '' for ch in w: f = FAKE_CYR.get(ch, ch) out += f if f is not None else TRANSLIT.get(ch, ch) return out def leet_word(self, w): out, used = '', 0 for ch in w: l = LEET.get(ch.lower()) if l and used < 2 and self.chance(45): out += l used += 1 else: out += ch return out def build_parts(self, lang, n): """-> список пар (слово, is_surname).""" w = lambda x: (x, False) s = lambda x: (x, True) if lang == 'ru': if n == 1: v = self.r.randrange(100) if v < 45: return [w(self.pick(RU_NOUN))] if v < 80: return [w(self.pick(RU_NAME))] return [s(self.pick(RU_SURNAME))] if n == 2: v = self.r.randrange(4) if v == 0: return [w(self.pick(RU_ADJ)), w(self.pick(RU_NOUN))] if v == 1: return [w(self.pick(RU_ADJ)), w(self.pick(RU_NAME))] if v == 2: return [w(self.pick(RU_NOUN)), w(self.pick(RU_NOUN))] return [w(self.pick(RU_NAME)), s(self.pick(RU_SURNAME))] # VasyaPupkin if self.chance(50): return [w(self.pick(RU_ADJ)), w(self.pick(RU_NOUN)), w(self.pick(RU_NOUN))] return [w(self.pick(RU_ADJ)), w(self.pick(RU_NAME)), s(self.pick(RU_SURNAME))] if n == 1: return [w(self.pick(EN_NOUN))] if n == 2: return [w(self.pick(EN_ADJ)), w(self.pick(EN_NOUN))] first = self.pick(EN_ADJ) second = self.pick([x for x in EN_ADJ if x != first]) if self.chance(50) else self.pick(EN_NOUN) return [w(first), w(second), w(self.pick(EN_NOUN))] def generate_one(self): total = max(sum(self.weights), 1) roll = self.r.randrange(total) n = 1 if roll < self.weights[0] else 2 if roll < self.weights[0] + self.weights[1] else 3 lang = 'ru' if self.chance(70) else 'en' parts = self.build_parts(lang, n) # редкая смесь языков в 2-3-частных никах if n > 1 and self.chance(15): parts[-1] = (self.pick(EN_NOUN), False) if lang == 'ru' else (self.translit_word(self.pick(RU_NOUN)), False) modes = ([['translit'], []][not self.translit]) + ([['fakeCyr'], []][not self.fake_cyr]) ru_mode = self.pick(modes) if modes else 'translit' rendered = [] for w, is_surname in parts: if RU_RE.search(w): if ru_mode == 'fakeCyr': rendered.append(self.fake_cyr_word(w)) else: t = self.translit_word(w) # фамилии на -ov: SmirnOFF / Smirnoff if is_surname and t.lower().endswith('ov') and self.chance(50): t = t[:-2] + ('OFF' if self.chance(40) else 'off') rendered.append(t) else: rendered.append(self.leet_word(w) if self.leet and self.chance(20) else w) nick = ''.join(rendered) # декорации (стиль сида: dashka, ZloyPES, HolodOK, OldRogue38, MCVovan95, xXDarkSlayerXx) if ru_mode == 'translit': if self.chance(12): nick = nick.lower() elif self.chance(12) and len(nick) > 4: k = 2 + self.r.randrange(2) nick = nick[:-k] + nick[-k:].upper() if self.digits and self.chance(28) and len(nick) <= 14: v = self.r.randrange(100) if v < 45: suf = str(1 + self.r.randrange(9)) if self.chance(50) else str(10 + self.r.randrange(90)) elif v < 80: suf = '%02d' % ((85 + self.r.randrange(23)) % 100) # год рождения 85..07 else: suf = str(1985 + self.r.randrange(23)) # 1985..2007 if len(nick) + len(suf) <= 16: nick += suf if self.chance(4) and len(nick) <= 13: nick += 'RUS' prefixed = False if self.chance(6) and len(nick) <= 14: nick = self.pick(PREFIXES) + nick prefixed = True if self.wrap and not prefixed and self.chance(4) and len(nick) <= 12: nick = 'xX' + nick + 'Xx' return nick def batch(self, count, taken=None): taken = taken if taken is not None else set() out = [] guard = count * 200 while len(out) < count and guard > 0: guard -= 1 nick = self.generate_one() if VALID.match(nick) and nick.lower() not in taken: taken.add(nick.lower()) out.append(nick) if len(out) < count: raise RuntimeError(f'Не хватило уникальных имён: {len(out)}/{count}. Расширь пулы слов.') return out # --- Перезапись сидов --------------------------------------------------------- XML_RE = re.compile(r'(