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:
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
@@ -110,6 +112,39 @@ func ValidateDisplayName(raw string) (string, error) {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// sanitizeDisplayName best-effort cleans a platform-supplied name (e.g. a Telegram
|
||||
// first name) to the editable display-name format: it keeps the maximal runs of
|
||||
// Unicode letters and joins them with a single space, dropping every other rune
|
||||
// (emoji, digits, punctuation), then caps the result to maxDisplayName runes. The
|
||||
// result therefore always satisfies ValidateDisplayName, or is empty when the input
|
||||
// carries no letters — in which case the caller substitutes placeholderDisplayName.
|
||||
// Mirroring the profile editor's rule means a connector-provisioned name is editable
|
||||
// later without first failing validation.
|
||||
func sanitizeDisplayName(raw string) string {
|
||||
fields := strings.FieldsFunc(raw, func(r rune) bool { return !unicode.IsLetter(r) })
|
||||
if len(fields) == 0 {
|
||||
return ""
|
||||
}
|
||||
name := strings.Join(fields, " ")
|
||||
if utf8.RuneCountInString(name) > maxDisplayName {
|
||||
name = strings.TrimRight(string([]rune(name)[:maxDisplayName]), " ")
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// placeholderDisplayName builds a fallback display name for a platform account whose
|
||||
// supplied name had no usable letters: "Player-NNNNN" for lang "en" (the default) or
|
||||
// "Игрок-NNNNN" for "ru", with five random digits. The generated name intentionally
|
||||
// carries digits and a hyphen, so it lies outside the editable format and the player
|
||||
// is expected to rename it; provisioned names bypass that editor validation.
|
||||
func placeholderDisplayName(lang string) string {
|
||||
prefix := "Player"
|
||||
if lang == "ru" {
|
||||
prefix = "Игрок"
|
||||
}
|
||||
return fmt.Sprintf("%s-%05d", prefix, rand.IntN(100000))
|
||||
}
|
||||
|
||||
// validateAwayWindow checks that the daily away window's duration, wrapping across
|
||||
// midnight, does not exceed maxAwayWindow. A zero-length window (start == end) means
|
||||
// "no away time" and is allowed.
|
||||
|
||||
Reference in New Issue
Block a user