SPP bot world as of 2026-08-26: merged state of all Cowork sessions (brain, combat data, chat seams 2.0, idiolect, heard/context, headless players, name generator, crests, seeds)
This commit is contained in:
+277
@@ -0,0 +1,277 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user