package robot import ( "math/rand/v2" "strconv" "scrabble/backend/internal/engine" ) // This file holds the wide, multi-locale corpus and the generator that gives each // auto-match robot a fresh, per-game display name (composeRandomName, reached through // Service.PickNamed). It is deliberately separate from the seeding pools in names.go: // those are a fixed 32-per-language set bound 1:1 to the durable robot accounts (a // stable fallback name), whereas this corpus only sources throwaway per-game names and // can grow freely. Because the chosen name is stored on the seat rather than on the // account, the small pool of accounts presents as an ever-changing crowd of opponents. // // Every composed name stays within account.ValidateDisplayName's editable format // (Unicode letters, single "." / "_" separators, an optional trailing run of up to five // digits), so a disguised robot remains indistinguishable from a human // (docs/ARCHITECTURE.md §7). A Russian-variant game draws Cyrillic names with at most // latinShareInRussian% Latin and never a CJK script; an English game draws the full // international corpus (Western Latin names, native Japanese/Chinese names, Latin handles). // nickSharePercent is the share of per-game names rendered as a handle (e.g. // "DarkWolf07") rather than a person's real name. const nickSharePercent = 35 // cjkShareIntl is the share of an international game's real names rendered in a native // Japanese or Chinese script; the remainder are Western Latin names. const cjkShareIntl = 25 // composeRandomName builds a fresh display name appropriate to the game variant (see // the file overview for the routing). The result always satisfies // account.ValidateDisplayName. func composeRandomName(variant engine.Variant) string { if variant == engine.VariantRussianScrabble || variant == engine.VariantErudit { if rand.IntN(100) < latinShareInRussian { return latinPersona() } return cyrillicPersona() } return intlPersona() } // cyrillicPersona returns a Cyrillic real name or handle. func cyrillicPersona() string { if rand.IntN(100) < nickSharePercent { return cyrillicNick() } return cyrillicReal() } // latinPersona returns a Western Latin real name or handle (also the Latin minority of // Russian games and the handles of international games). func latinPersona() string { if rand.IntN(100) < nickSharePercent { return latinNick() } return latinReal() } // intlPersona returns an international name: a Latin handle, a native Japanese/Chinese // real name, or a Western Latin real name. func intlPersona() string { if rand.IntN(100) < nickSharePercent { return latinNick() } if rand.IntN(100) < cjkShareIntl { return cjkReal() } return latinReal() } // latinReal composes a Western full name within a single locale so it reads naturally. func latinReal() string { loc := latinLocales[rand.IntN(len(latinLocales))] first := loc.first[rand.IntN(len(loc.first))] surname := loc.surnames[rand.IntN(len(loc.surnames))] return composeName(first, surname, rand.IntN(3)) } // cyrillicReal composes a Russian full name, agreeing the surname form with the first // name's grammatical gender, in one of the three rendering forms. func cyrillicReal() string { fn := firstNamesRUWide[rand.IntN(len(firstNamesRUWide))] sp := surnamesRUWide[rand.IntN(len(surnamesRUWide))] surname := sp.m if fn.female { surname = sp.f } return composeName(fn.name, surname, rand.IntN(3)) } // cjkReal composes a native Japanese or Chinese name. func cjkReal() string { if rand.IntN(2) == 0 { return japaneseName() } return chineseName() } // japaneseName composes a Japanese name: family-then-given (space-joined), or a bare // family or given name as a handle. func japaneseName() string { family := jpSurnames[rand.IntN(len(jpSurnames))] given := jpGiven[rand.IntN(len(jpGiven))] switch rand.IntN(10) { case 0, 1, 2: return given case 3: return family default: return family + " " + given } } // chineseName composes a Chinese name: family then given, written without a separator. func chineseName() string { family := cnSurnames[rand.IntN(len(cnSurnames))] given := cnGiven[rand.IntN(len(cnGiven))] return family + given } // latinNick builds a Latin handle from the adjective and noun stem pools. English // adjectives do not inflect, so any pairing reads naturally. func latinNick() string { return assembleHandle(adjLatin[rand.IntN(len(adjLatin))], nounLatin[rand.IntN(len(nounLatin))]) } // cyrillicNick builds a Cyrillic handle, agreeing the adjective's form with the noun's // grammatical gender ("Дикий Волк", "Дикая Рысь"). func cyrillicNick() string { noun := nounCyr[rand.IntN(len(nounCyr))] adj := adjCyr[rand.IntN(len(adjCyr))] form := adj.m if noun.female { form = adj.f } return assembleHandle(form, noun.word) } // assembleHandle joins an adjective and noun into a handle: a bare noun, the two // camel-cased ("DarkWolf"), or joined by a separator. Between two meaningful words a human // almost always uses "_" — a "." there is a rare tell — so the separator is an underscore // far more often than a dot. An optional trailing number may follow. func assembleHandle(adj, noun string) string { var base string switch rand.IntN(4) { case 0: base = noun // a bare noun case 1: base = adj + noun // camel-cased default: sep := "_" if rand.IntN(10) == 0 { sep = "." // a dot between two words is rare } base = adj + sep + noun } if rand.IntN(2) == 0 { base += nickNumber() } return base } // nickNumber returns a short trailing number a handle commonly carries — a single // digit, a two-digit suffix, or a four-digit year — always within the validator's // five-digit cap. func nickNumber() string { switch rand.IntN(3) { case 0: return strconv.Itoa(rand.IntN(9) + 1) // 1–9 case 1: return strconv.Itoa(rand.IntN(90) + 10) // 10–99 default: return strconv.Itoa(rand.IntN(40) + 1985) // 1985–2024 } } // latinLocale is one Western locale's first names and surnames, paired only within the // locale so a composed full name reads naturally. type latinLocale struct { first []string surnames []string } // latinLocales are the Western (Latin-script) locales the international and Russian-Latin // pools draw from: English (reusing the seeding pools), German, Spanish, Italian, // French, Portuguese. var latinLocales = []latinLocale{ {first: firstNamesFullEN, surnames: surnamesEN}, { // German first: []string{"Lukas", "Leon", "Finn", "Jonas", "Felix", "Maximilian", "Paul", "Elias", "Noah", "Ben", "Klaus", "Wolfgang", "Hannah", "Emma", "Mia", "Sofia", "Lena", "Marie", "Laura", "Greta", "Ingrid", "Stefan"}, surnames: []string{"Müller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", "Becker", "Hoffmann", "Koch", "Richter", "Bauer", "Klein", "Wolf", "Schröder", "Neumann", "Braun", "Werner", "Krüger", "Hartmann", "Lange", "Schulze"}, }, { // Spanish first: []string{"Mateo", "Hugo", "Martín", "Lucas", "Daniel", "Pablo", "Álvaro", "Diego", "Javier", "Carlos", "Lucía", "Sofía", "María", "Martina", "Paula", "Valeria", "Carmen", "Elena", "Sara", "Pilar", "Andrés", "Sergio"}, surnames: []string{"García", "Martínez", "López", "Sánchez", "González", "Rodríguez", "Fernández", "Gómez", "Jiménez", "Ruiz", "Hernández", "Díaz", "Moreno", "Álvarez", "Romero", "Alonso", "Navarro", "Torres", "Ramos", "Serrano", "Castro", "Rubio"}, }, { // Italian first: []string{"Francesco", "Alessandro", "Lorenzo", "Mattia", "Andrea", "Leonardo", "Gabriele", "Riccardo", "Tommaso", "Matteo", "Sofia", "Giulia", "Aurora", "Alice", "Emma", "Giorgia", "Martina", "Chiara", "Giuseppe", "Marco", "Roberto", "Stefano"}, surnames: []string{"Rossi", "Russo", "Ferrari", "Esposito", "Bianchi", "Romano", "Colombo", "Ricci", "Marino", "Greco", "Bruno", "Gallo", "Conti", "Costa", "Giordano", "Mancini", "Rizzo", "Lombardi", "Moretti", "Barbieri", "Fontana", "Caruso"}, }, { // French first: []string{"Gabriel", "Louis", "Raphaël", "Jules", "Adam", "Arthur", "Nathan", "Léo", "Emma", "Jade", "Louise", "Alice", "Chloé", "Léa", "Manon", "Camille", "Pierre", "Sophie", "Julien", "Céline", "Antoine", "Claire"}, surnames: []string{"Martin", "Bernard", "Dubois", "Robert", "Richard", "Petit", "Durand", "Leroy", "Moreau", "Simon", "Laurent", "Lefebvre", "Michel", "Bertrand", "Roux", "Vincent", "Fournier", "Morel", "Girard", "Mercier", "Lambert", "Bonnet"}, }, { // Portuguese first: []string{"João", "Pedro", "Tiago", "Miguel", "Tomás", "Rodrigo", "Diogo", "Afonso", "Duarte", "Maria", "Leonor", "Matilde", "Beatriz", "Carolina", "Mariana", "Inês", "Sofia", "Lara", "António", "Rita", "Catarina", "Bruno"}, surnames: []string{"Silva", "Santos", "Ferreira", "Pereira", "Oliveira", "Costa", "Rodrigues", "Martins", "Sousa", "Fernandes", "Gonçalves", "Gomes", "Lopes", "Marques", "Alves", "Almeida", "Ribeiro", "Pinto", "Carvalho", "Teixeira", "Moreira", "Correia"}, }, } // jpSurnames and jpGiven are common Japanese family and given names in kanji. var jpSurnames = []string{ "佐藤", "鈴木", "高橋", "田中", "渡辺", "伊藤", "山本", "中村", "小林", "加藤", "吉田", "山田", "山口", "松本", "井上", "木村", "斎藤", "清水", "山崎", "池田", "橋本", "阿部", } var jpGiven = []string{ "大翔", "蓮", "陽翔", "悠真", "颯太", "湊", "翔太", "大和", "健太", "樹", "葵", "陽菜", "凛", "結衣", "美咲", "優奈", "七海", "真央", "愛", "杏", } // cnSurnames and cnGiven are common Chinese family and given names in Han characters. var cnSurnames = []string{ "王", "李", "张", "刘", "陈", "杨", "黄", "赵", "周", "吴", "徐", "孙", "胡", "朱", "高", "林", "何", "郭", "马", "罗", "梁", "宋", "郑", "韩", } var cnGiven = []string{ "伟", "芳", "娜", "敏", "静", "丽", "强", "磊", "军", "洋", "勇", "艳", "杰", "娟", "涛", "明", "超", "霞", "刚", "婷", "秀英", "建华", } // adjLatin and nounLatin are the adjective and noun stems for Latin handles. var adjLatin = []string{ "Dark", "Night", "Shadow", "Frost", "Silent", "Swift", "Wild", "Royal", "Lucky", "Crimson", "Mystic", "Cosmic", "Epic", "Noble", "Brave", "Lone", "Iron", "Steel", "Golden", "Silver", "Neon", "Cyber", "Turbo", "Rapid", "Hidden", "Frozen", "Electric", "Savage", } var nounLatin = []string{ "Wolf", "Fox", "Raven", "Tiger", "Eagle", "Dragon", "Falcon", "Hawk", "Lion", "Bear", "Panther", "Viper", "Cobra", "Shark", "Phoenix", "Knight", "Ranger", "Hunter", "Rider", "Ghost", "Phantom", "Comet", "Nova", "Blade", "Arrow", "Storm", "Wizard", "Ninja", } // cyrAdjective is a Russian adjective in its masculine and feminine forms, so a handle's // stem can agree with the gender of its noun. type cyrAdjective struct{ m, f string } // cyrNoun is a Russian noun for a handle, tagged feminine so the paired adjective agrees. type cyrNoun struct { word string female bool } // adjCyr and nounCyr are the adjective and noun stems for Cyrillic handles; cyrillicNick // renders the adjective in the form that agrees with the chosen noun's gender. var adjCyr = []cyrAdjective{ {"Тёмный", "Тёмная"}, {"Ночной", "Ночная"}, {"Снежный", "Снежная"}, {"Лунный", "Лунная"}, {"Звёздный", "Звёздная"}, {"Огненный", "Огненная"}, {"Ледяной", "Ледяная"}, {"Стальной", "Стальная"}, {"Дикий", "Дикая"}, {"Тихий", "Тихая"}, {"Быстрый", "Быстрая"}, {"Гордый", "Гордая"}, {"Грозный", "Грозная"}, {"Хитрый", "Хитрая"}, {"Смелый", "Смелая"}, {"Весёлый", "Весёлая"}, {"Рыжий", "Рыжая"}, {"Серый", "Серая"}, {"Золотой", "Золотая"}, {"Дерзкий", "Дерзкая"}, {"Лютый", "Лютая"}, {"Бравый", "Бравая"}, {"Мудрый", "Мудрая"}, {"Шальной", "Шальная"}, {"Вольный", "Вольная"}, {"Яростный", "Яростная"}, {"Седой", "Седая"}, {"Жгучий", "Жгучая"}, } var nounCyr = []cyrNoun{ {"Волк", false}, {"Лис", false}, {"Ворон", false}, {"Тигр", false}, {"Орёл", false}, {"Дракон", false}, {"Сокол", false}, {"Ястреб", false}, {"Лев", false}, {"Медведь", false}, {"Барс", false}, {"Кот", false}, {"Кит", false}, {"Странник", false}, {"Охотник", false}, {"Рыцарь", false}, {"Маг", false}, {"Призрак", false}, {"Воин", false}, {"Клинок", false}, {"Ветер", false}, {"Гром", false}, {"Шторм", false}, {"Пилот", false}, {"Капитан", false}, {"Мастер", false}, {"Енот", false}, {"Комета", true}, {"Звезда", true}, {"Молния", true}, {"Пантера", true}, {"Рысь", true}, {"Буря", true}, {"Сова", true}, {"Акула", true}, } // firstNamesRUWide is a broad pool of Russian first names — full and colloquial forms // mixed, each tagged by grammatical gender — for the per-game generator (wider than the // 32-slot seeding pool in names.go). var firstNamesRUWide = []genderedName{ {"Александр", false}, {"Саша", false}, {"Дмитрий", false}, {"Дима", false}, {"Сергей", false}, {"Серёжа", false}, {"Алексей", false}, {"Лёша", false}, {"Михаил", false}, {"Миша", false}, {"Николай", false}, {"Коля", false}, {"Владимир", false}, {"Володя", false}, {"Константин", false}, {"Костя", false}, {"Павел", false}, {"Паша", false}, {"Андрей", false}, {"Андрюша", false}, {"Иван", false}, {"Ваня", false}, {"Пётр", false}, {"Петя", false}, {"Роман", false}, {"Рома", false}, {"Антон", false}, {"Евгений", false}, {"Женя", false}, {"Максим", false}, {"Кирилл", false}, {"Артём", false}, {"Анна", true}, {"Аня", true}, {"Мария", true}, {"Маша", true}, {"Анастасия", true}, {"Настя", true}, {"Наталья", true}, {"Наташа", true}, {"Татьяна", true}, {"Таня", true}, {"Екатерина", true}, {"Катя", true}, {"Юлия", true}, {"Юля", true}, {"Ольга", true}, {"Оля", true}, {"Елена", true}, {"Лена", true}, {"Ирина", true}, {"Ира", true}, {"Светлана", true}, {"Света", true}, {"Дарья", true}, {"Даша", true}, {"София", true}, {"Соня", true}, {"Полина", true}, {"Алина", true}, {"Вера", true}, {"Ксения", true}, } // surnamesRUWide is a broad pool of Russian surnames in masculine and feminine forms // for the per-game generator. var surnamesRUWide = []surnamePair{ {"Иванов", "Иванова"}, {"Смирнов", "Смирнова"}, {"Кузнецов", "Кузнецова"}, {"Соколов", "Соколова"}, {"Попов", "Попова"}, {"Лебедев", "Лебедева"}, {"Новиков", "Новикова"}, {"Морозов", "Морозова"}, {"Волков", "Волкова"}, {"Зайцев", "Зайцева"}, {"Павлов", "Павлова"}, {"Беляев", "Беляева"}, {"Орлов", "Орлова"}, {"Громов", "Громова"}, {"Суханов", "Суханова"}, {"Киселёв", "Киселёва"}, {"Макаров", "Макарова"}, {"Фёдоров", "Фёдорова"}, {"Никитин", "Никитина"}, {"Захаров", "Захарова"}, {"Борисов", "Борисова"}, {"Королёв", "Королёва"}, {"Герасимов", "Герасимова"}, {"Пономарёв", "Пономарёва"}, {"Григорьев", "Григорьева"}, {"Романов", "Романова"}, {"Виноградов", "Виноградова"}, {"Богданов", "Богданова"}, {"Воробьёв", "Воробьёва"}, {"Сергеев", "Сергеева"}, {"Кузьмин", "Кузьмина"}, {"Соловьёв", "Соловьёва"}, {"Васильев", "Васильева"}, {"Михайлов", "Михайлова"}, {"Петров", "Петрова"}, {"Тарасов", "Тарасова"}, {"Белов", "Белова"}, {"Комаров", "Комарова"}, {"Гусев", "Гусева"}, {"Титов", "Титова"}, }