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:
2026-09-03 19:05:51 +02:00
parent f520c66e85
commit dcbecd2843
55 changed files with 58741 additions and 566 deletions
+277
View File
@@ -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())