Stage 17: backend defect fixes (nudge code, TG name, robot names/timing, multi-device push, move-duration metric + admin analytics)
- #3 nudge-on-own-turn: distinct result code nudge_own_turn + i18n (was reused 'not_your_turn') - #2 sanitize connector registration name to the editable format; Player/Игрок-XXXXX fallback - #5 variant-aware robot name pools (composed full/colloquial first + surname forms; ru gets <=20% latin) - #4 move-number-aware robot move timing (early 1-5min -> late 10-90min, skew k=4) - #7 emit move event to the actor too (multi-device sync); opponent_moved stays in-app only - #1 live game_move_duration{variant,phase} histogram + admin console per-user min/avg/max columns and an inline-SVG move-time-by-move-number chart (offline from the journal) - ProvisionRobot bypasses editor name validation (system names like 'Peter J.')
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
package robot
|
||||
|
||||
// Robot display names are composed, not hand-listed. Per language there is a pool of
|
||||
// 32 full first names and a paired pool of 32 colloquial forms (William/Bill,
|
||||
// Анастасия/Настя), a surname pool, and three rendering forms: first name only;
|
||||
// first name plus a surname initial; first name plus full surname. Because robots are
|
||||
// durable accounts whose name must stay stable across restarts (a player's opponent
|
||||
// must not rename itself on every deploy, nor mid-game), the composition is
|
||||
// deterministic per pool slot — seeded by the slot index through mix — rather than
|
||||
// re-randomised each boot. Russian surnames are gender-agreed with the first name.
|
||||
|
||||
// robotPoolSize is the number of robot accounts provisioned per language. It equals
|
||||
// the first-name pool size, so each slot draws a distinct person.
|
||||
const robotPoolSize = 32
|
||||
|
||||
// latinShareInRussian is the approximate percentage of Russian-variant games that
|
||||
// draw a Latin-named robot rather than a Russian-named one (the owner's "≤20%").
|
||||
const latinShareInRussian = 20
|
||||
|
||||
// name composition forms.
|
||||
const (
|
||||
nameFormFirstOnly = iota // "Anna"
|
||||
nameFormInitial // "Anna C."
|
||||
nameFormFull // "Anna Carter"
|
||||
)
|
||||
|
||||
// genderedName is a Russian first name tagged by grammatical gender so the surname
|
||||
// form (masculine vs feminine) can agree with it.
|
||||
type genderedName struct {
|
||||
name string
|
||||
female bool
|
||||
}
|
||||
|
||||
// surnamePair holds a Russian surname's masculine and feminine forms.
|
||||
type surnamePair struct{ m, f string }
|
||||
|
||||
// firstNamesFullEN and firstNamesShortEN are paired by index: the same person's
|
||||
// official and colloquial English first name (William/Bill).
|
||||
var firstNamesFullEN = []string{
|
||||
"William", "Robert", "Thomas", "Nicholas", "Joseph", "Edward", "Charles", "Margaret",
|
||||
"Elizabeth", "Katherine", "Alexander", "Samuel", "Andrew", "Christopher", "Benjamin", "Daniel",
|
||||
"Matthew", "Anthony", "Michael", "Richard", "Jonathan", "Patricia", "Jennifer", "Jessica",
|
||||
"Stephanie", "Victoria", "Theodore", "Frederick", "Gabriel", "Vincent", "Eleanor", "Josephine",
|
||||
}
|
||||
|
||||
var firstNamesShortEN = []string{
|
||||
"Will", "Bob", "Tom", "Nick", "Joe", "Eddie", "Charlie", "Maggie",
|
||||
"Liz", "Kate", "Alex", "Sam", "Drew", "Chris", "Ben", "Dan",
|
||||
"Matt", "Tony", "Mike", "Rick", "Jon", "Pat", "Jen", "Jess",
|
||||
"Steph", "Vicky", "Ted", "Fred", "Gabe", "Vince", "Ellie", "Josie",
|
||||
}
|
||||
|
||||
// surnamesEN is a pool of gender-neutral English surnames.
|
||||
var surnamesEN = []string{
|
||||
"Carter", "Hayes", "Brooks", "Reed", "Cole", "Lane", "Bishop", "Hart",
|
||||
"Shaw", "Wells", "Pierce", "Wade", "Frost", "Doyle", "Boyd", "Marsh",
|
||||
"Hale", "Nash", "Webb", "Dale", "Park", "Lyon", "Snow", "Cross",
|
||||
"Vance", "Knox", "Page", "Ford", "Banks", "Foster", "Greer", "Mills",
|
||||
}
|
||||
|
||||
// firstNamesFullRU and firstNamesShortRU are paired by index: the same person's
|
||||
// official and colloquial Russian first name (Анастасия/Настя), gender-tagged.
|
||||
var firstNamesFullRU = []genderedName{
|
||||
{"Александр", 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},
|
||||
}
|
||||
|
||||
var firstNamesShortRU = []genderedName{
|
||||
{"Саша", 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},
|
||||
}
|
||||
|
||||
// surnamesRU is a pool of common Russian surnames in masculine and feminine forms.
|
||||
var surnamesRU = []surnamePair{
|
||||
{"Иванов", "Иванова"}, {"Смирнов", "Смирнова"}, {"Кузнецов", "Кузнецова"},
|
||||
{"Соколов", "Соколова"}, {"Попов", "Попова"}, {"Лебедев", "Лебедева"},
|
||||
{"Новиков", "Новикова"}, {"Морозов", "Морозова"}, {"Волков", "Волкова"},
|
||||
{"Зайцев", "Зайцева"}, {"Павлов", "Павлова"}, {"Беляев", "Беляева"},
|
||||
{"Орлов", "Орлова"}, {"Громов", "Громова"}, {"Суханов", "Суханова"},
|
||||
{"Киселёв", "Киселёва"}, {"Макаров", "Макарова"}, {"Фёдоров", "Фёдорова"},
|
||||
{"Никитин", "Никитина"}, {"Захаров", "Захарова"}, {"Борисов", "Борисова"},
|
||||
{"Королёв", "Королёва"}, {"Герасимов", "Герасимова"}, {"Пономарёв", "Пономарёва"},
|
||||
{"Григорьев", "Григорьева"}, {"Романов", "Романова"}, {"Виноградов", "Виноградова"},
|
||||
{"Богданов", "Богданова"}, {"Воробьёв", "Воробьёва"}, {"Сергеев", "Сергеева"},
|
||||
{"Кузьмин", "Кузьмина"}, {"Соловьёв", "Соловьёва"},
|
||||
}
|
||||
|
||||
// robotDisplayNamesEN builds the English robot display-name pool, one per slot. Each
|
||||
// slot draws its paired full or colloquial first name, a surname, and a form.
|
||||
func robotDisplayNamesEN() []string {
|
||||
out := make([]string, robotPoolSize)
|
||||
for i := range out {
|
||||
h := mix(int64(i), "robot-en")
|
||||
first := firstNamesFullEN[i%len(firstNamesFullEN)]
|
||||
if (h>>16)&1 == 1 {
|
||||
first = firstNamesShortEN[i%len(firstNamesShortEN)]
|
||||
}
|
||||
surname := surnamesEN[h%uint64(len(surnamesEN))]
|
||||
out[i] = composeName(first, surname, int((h>>8)%3))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// robotDisplayNamesRU builds the Russian robot display-name pool, one per slot, with
|
||||
// the surname form agreeing with the first name's gender.
|
||||
func robotDisplayNamesRU() []string {
|
||||
out := make([]string, robotPoolSize)
|
||||
for i := range out {
|
||||
h := mix(int64(i), "robot-ru")
|
||||
fn := firstNamesFullRU[i%len(firstNamesFullRU)]
|
||||
if (h>>16)&1 == 1 {
|
||||
fn = firstNamesShortRU[i%len(firstNamesShortRU)]
|
||||
}
|
||||
sp := surnamesRU[h%uint64(len(surnamesRU))]
|
||||
surname := sp.m
|
||||
if fn.female {
|
||||
surname = sp.f
|
||||
}
|
||||
out[i] = composeName(fn.name, surname, int((h>>8)%3))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// composeName renders one of the three name forms from a first name and a surname.
|
||||
func composeName(first, surname string, form int) string {
|
||||
switch form {
|
||||
case nameFormInitial:
|
||||
return first + " " + string([]rune(surname)[:1]) + "."
|
||||
case nameFormFull:
|
||||
return first + " " + surname
|
||||
default:
|
||||
return first
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user