feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
+102
View File
@@ -0,0 +1,102 @@
package racename
import (
"fmt"
"strings"
"galaxy/util"
confusables "github.com/disciplinedware/go-confusables"
"golang.org/x/text/cases"
)
// confusableSkeletoner abstracts the underlying TR39 confusable-skeleton
// computer so tests may substitute a deterministic stub.
type confusableSkeletoner interface {
Skeleton(string) string
}
// Policy produces canonical uniqueness keys and validates user-supplied race
// names under the Race Name Directory rules: Unicode case folding, explicit
// ASCII anti-fraud digit-to-letter mappings, and a TR39 confusable skeleton.
type Policy struct {
caseFolder cases.Caser
skeletoner confusableSkeletoner
}
// antiFraudReplacer collapses the frozen ASCII anti-fraud digit-to-letter
// pairs so `P1lot` and `Pilot` canonicalize together.
var antiFraudReplacer = strings.NewReplacer(
"1", "i",
"0", "o",
"8", "b",
)
// NewPolicy returns the default race-name canonicalization policy.
func NewPolicy() (*Policy, error) {
policy := &Policy{
caseFolder: cases.Fold(),
skeletoner: confusables.Default(),
}
if policy.skeletoner == nil {
return nil, fmt.Errorf("new race-name policy: nil confusable skeletoner")
}
return policy, nil
}
// Canonical returns the stable uniqueness key for raceName.
//
// raceName is expected to be non-empty; surrounding whitespace is trimmed
// before canonicalization so callers that preserve original casing pass the
// untrimmed display form directly.
func (policy *Policy) Canonical(raceName string) (CanonicalKey, error) {
switch {
case policy == nil:
return "", fmt.Errorf("canonicalize race name: nil policy")
case policy.skeletoner == nil:
return "", fmt.Errorf("canonicalize race name: nil confusable skeletoner")
}
trimmed := strings.TrimSpace(raceName)
if trimmed == "" {
return "", fmt.Errorf("canonicalize race name: race name must not be empty")
}
folded := policy.caseFolder.String(trimmed)
antiFraudMapped := antiFraudReplacer.Replace(folded)
key := CanonicalKey(policy.skeletoner.Skeleton(antiFraudMapped))
if err := key.Validate(); err != nil {
return "", fmt.Errorf("canonicalize race name: %w", err)
}
return key, nil
}
// ValidateName reports whether raceName is structurally valid for use in the
// Race Name Directory. It delegates to galaxy/util.ValidateTypeName and
// returns the trimmed canonical display value on success.
func ValidateName(raceName string) (string, error) {
trimmed, ok := util.ValidateTypeName(raceName)
if !ok {
return "", fmt.Errorf("race name is invalid")
}
return trimmed, nil
}
// Canonicalize validates raceName as a Race Name Directory display value
// and returns its canonical uniqueness key. It composes ValidateName with
// the Canonical pipeline so every RND write and lookup shares a single
// entry point for both character-set and confusable-pair policy.
//
// Invalid raceName values surface the error returned by ValidateName;
// callers at the RaceNameDirectory port boundary map these to
// ports.ErrInvalidName.
func (policy *Policy) Canonicalize(raceName string) (CanonicalKey, error) {
trimmed, err := ValidateName(raceName)
if err != nil {
return "", fmt.Errorf("canonicalize race name: %w", err)
}
return policy.Canonical(trimmed)
}