feat(robot): per-game display names from a wide name corpus
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 48s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m10s

Decouple the displayed opponent name from the small pool of durable robot
accounts: the disguised auto-match robot now gets a freshly composed name each
game, stamped on a new game_players.display_name seat snapshot. The snapshot
also captures humans' names, freezing what an opponent sees for the life of a
game (a later rename no longer rewrites past games); readers fall back to the
account's current name for pre-migration rows.

Names come from a wide composed corpus (internal/robot/namevariety.go): Western
locales (EN/DE/ES/IT/FR/PT), native Japanese/Chinese names, a gender-agreed
Russian pool, and human-style handles. Routing keeps Pick's spirit -- a Russian
game draws Cyrillic + <=20% Latin and never a CJK script; an English game the
full corpus -- via robot.PickNamed.

Loosen account.ValidateDisplayName (and the UI mirror) to admit a trailing run
of up to five digits, so "Player2007"-style handles are valid for humans too
and the disguised robot stays indistinguishable.

Migration 00007 adds game_players.display_name (additive, NOT NULL DEFAULT '');
jet regenerated. Docs (ARCHITECTURE 7, FUNCTIONAL + _ru, PLAN, README) updated.
This commit is contained in:
Ilia Denisov
2026-06-16 12:28:04 +02:00
parent ac1c89c0ee
commit 183e08ec80
24 changed files with 811 additions and 123 deletions
+3
View File
@@ -33,6 +33,9 @@ type GameCreator interface {
// available so the matchmaker can defer substitution.
type RobotProvider interface {
Pick(variant engine.Variant) (uuid.UUID, error)
// PickNamed selects a robot and composes a fresh per-game display name for it, for
// the disguised auto-match path (the name is stamped on the robot's seat).
PickNamed(variant engine.Variant) (uuid.UUID, string, error)
}
// Blocker reports whether two accounts have a block between them (either
+3 -3
View File
@@ -19,7 +19,7 @@ import (
// it.
type GameMatcher interface {
OpenOrJoin(ctx context.Context, accountID uuid.UUID, params game.CreateParams, openDeadline time.Time) (game.Game, bool, error)
AttachRobot(ctx context.Context, gameID, robotID uuid.UUID) (game.Game, bool, error)
AttachRobot(ctx context.Context, gameID, robotID uuid.UUID, displayName string) (game.Game, bool, error)
ExpiredOpen(ctx context.Context, now time.Time) ([]game.OpenGame, error)
InitialState(ctx context.Context, gameID, accountID uuid.UUID) (notify.PlayerState, error)
// Create seats the given accounts in an active game at once; the AI-match path uses
@@ -148,12 +148,12 @@ func (m *Matchmaker) Reap(ctx context.Context, now time.Time) {
return
}
for _, og := range due {
robotID, err := m.robots.Pick(og.Variant)
robotID, name, err := m.robots.PickNamed(og.Variant)
if err != nil {
m.log.Warn("robot substitution deferred", zap.Error(err))
continue
}
g, attached, err := m.games.AttachRobot(ctx, og.ID, robotID)
g, attached, err := m.games.AttachRobot(ctx, og.ID, robotID, name)
if err != nil {
m.log.Warn("robot substitution failed", zap.String("game", og.ID.String()), zap.Error(err))
continue
+6 -1
View File
@@ -45,7 +45,7 @@ func (s *stubMatcher) OpenOrJoin(_ context.Context, _ uuid.UUID, _ game.CreatePa
return s.openGame, s.openJoined, s.openErr
}
func (s *stubMatcher) AttachRobot(_ context.Context, gameID, _ uuid.UUID) (game.Game, bool, error) {
func (s *stubMatcher) AttachRobot(_ context.Context, gameID, _ uuid.UUID, _ string) (game.Game, bool, error) {
if s.attachErr != nil {
return game.Game{}, false, s.attachErr
}
@@ -85,6 +85,11 @@ func (f *fakeRobots) Pick(variant engine.Variant) (uuid.UUID, error) {
return f.id, nil
}
func (f *fakeRobots) PickNamed(variant engine.Variant) (uuid.UUID, string, error) {
id, err := f.Pick(variant)
return id, "Robot", err
}
// capturePub records every published intent.
type capturePub struct{ intents []notify.Intent }