Files
L2-SPP/nickgen.py
T

278 lines
14 KiB
Python
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.
#!/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 (<character name="...">)
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'(<character id="(\d+)" name=")([^"]*)(")')
SQL_RE = re.compile(r"(VALUES \((270\d{6}), 'sppbots', ')([^']*)(')")
def rewrite(xml_path, sql_path, names_by_id, apply_changes):
xml_text = xml_path.read_text(encoding='utf-8')
sql_text = sql_path.read_text(encoding='utf-8')
def xml_sub(m):
return m.group(1) + names_by_id[int(m.group(2))] + m.group(4)
def sql_sub(m):
return m.group(1) + names_by_id[int(m.group(2)) - CHAR_ID_BASE] + m.group(4)
new_xml, n_xml = XML_RE.subn(xml_sub, xml_text)
new_sql, n_sql = SQL_RE.subn(sql_sub, sql_text)
if n_xml != len(names_by_id) or n_sql != len(names_by_id):
raise RuntimeError(f'Несогласованность: XML={n_xml}, SQL={n_sql}, имён={len(names_by_id)}')
if apply_changes:
xml_path.write_text(new_xml, encoding='utf-8', newline='')
sql_path.write_text(new_sql, encoding='utf-8', newline='')
return n_xml, n_sql
def main():
p = argparse.ArgumentParser(description='Генератор ников для ботов')
p.add_argument('--print', dest='print_n', type=int, metavar='N', help='напечатать N ников и выйти')
p.add_argument('--apply', action='store_true', help='переписать имена в XML и SQL (по умолчанию dry-run)')
p.add_argument('--seed', type=int, help='сид для воспроизводимости (по умолчанию случайный)')
p.add_argument('--xml', type=Path, default=XML_PATH)
p.add_argument('--sql', type=Path, default=SQL_PATH)
a = p.parse_args()
seed = a.seed if a.seed is not None else random.randrange(1 << 31)
gen = NickGen(seed=seed)
print(f'seed = {seed}')
if a.print_n:
for nick in gen.batch(a.print_n):
print(nick)
return
ids = [int(m.group(2)) for m in XML_RE.finditer(a.xml.read_text(encoding='utf-8'))]
names = gen.batch(len(ids))
names_by_id = dict(zip(ids, names))
n_xml, n_sql = rewrite(a.xml, a.sql, names_by_id, a.apply)
sample = ', '.join(names[:10])
if a.apply:
print(f'Переписано имён: XML={n_xml}, SQL={n_sql}. Первые: {sample}')
print('В работающей БД имена не изменятся (INSERT IGNORE) — нужен reset-bots.ps1 и пересборка образа.')
else:
print(f'Dry-run: заменил бы XML={n_xml}, SQL={n_sql} имён. Первые: {sample}')
print('Запусти с --apply чтобы записать.')
if __name__ == '__main__':
sys.exit(main())