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)
}
@@ -0,0 +1,188 @@
package racename
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestPolicyCanonicalCollisions(t *testing.T) {
t.Parallel()
policy, err := NewPolicy()
require.NoError(t, err)
tests := []struct {
name string
left string
right string
}{
{
name: "case insensitive collision",
left: "Pilot Nova",
right: "pilot nova",
},
{
name: "ascii anti fraud collision",
left: "Pilot Nova",
right: "P1lot N0va",
},
{
name: "unicode confusable collision",
left: "paypal",
right: "раураl",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
leftKey, err := policy.Canonical(tt.left)
require.NoError(t, err)
rightKey, err := policy.Canonical(tt.right)
require.NoError(t, err)
require.Equal(t, rightKey, leftKey)
})
}
}
func TestPolicyCanonicalRejectsEmpty(t *testing.T) {
t.Parallel()
policy, err := NewPolicy()
require.NoError(t, err)
_, err = policy.Canonical("")
require.Error(t, err)
_, err = policy.Canonical(" ")
require.Error(t, err)
}
func TestPolicyCanonicalTrimsWhitespace(t *testing.T) {
t.Parallel()
policy, err := NewPolicy()
require.NoError(t, err)
trimmed, err := policy.Canonical("Pilot Nova")
require.NoError(t, err)
padded, err := policy.Canonical(" Pilot Nova ")
require.NoError(t, err)
require.Equal(t, trimmed, padded)
}
func TestValidateNameDelegatesToUtil(t *testing.T) {
t.Parallel()
trimmed, err := ValidateName(" PilotNova ")
require.NoError(t, err)
require.Equal(t, "PilotNova", trimmed)
_, err = ValidateName("")
require.Error(t, err)
_, err = ValidateName(" ")
require.Error(t, err)
// Internal whitespace is rejected by util.ValidateTypeName.
_, err = ValidateName("Pilot Nova")
require.Error(t, err)
}
func TestCanonicalKeyValidate(t *testing.T) {
t.Parallel()
require.Error(t, CanonicalKey("").Validate())
require.Error(t, CanonicalKey(" abc").Validate())
require.Error(t, CanonicalKey("abc ").Validate())
require.NoError(t, CanonicalKey("abc").Validate())
require.True(t, CanonicalKey("").IsZero())
require.False(t, CanonicalKey("abc").IsZero())
require.Equal(t, "abc", CanonicalKey("abc").String())
}
func TestPolicyCanonicalizeValid(t *testing.T) {
t.Parallel()
policy, err := NewPolicy()
require.NoError(t, err)
key, err := policy.Canonicalize(" PilotNova ")
require.NoError(t, err)
require.NoError(t, key.Validate())
require.False(t, key.IsZero())
paddedKey, err := policy.Canonicalize("PilotNova")
require.NoError(t, err)
require.Equal(t, paddedKey, key)
}
func TestPolicyCanonicalizeEquivalences(t *testing.T) {
t.Parallel()
policy, err := NewPolicy()
require.NoError(t, err)
cases := []struct {
name string
left string
right string
}{
{
name: "case insensitive collision",
left: "PilotNova",
right: "pilotnova",
},
{
name: "ascii anti fraud collision",
left: "PilotNova",
right: "P1l0tN0va",
},
{
name: "unicode confusable collision",
left: "paypal",
right: "раураl",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
leftKey, err := policy.Canonicalize(tt.left)
require.NoError(t, err)
rightKey, err := policy.Canonicalize(tt.right)
require.NoError(t, err)
require.Equal(t, leftKey, rightKey)
})
}
}
func TestPolicyCanonicalizeRejectsInvalid(t *testing.T) {
t.Parallel()
policy, err := NewPolicy()
require.NoError(t, err)
invalid := []struct {
name string
input string
}{
{name: "empty", input: ""},
{name: "whitespace only", input: " "},
{name: "internal space", input: "Pilot Nova"},
{name: "leading dash", input: "-Pilot"},
{name: "trailing dash", input: "Pilot-"},
}
for _, tt := range invalid {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, err := policy.Canonicalize(tt.input)
require.Error(t, err)
})
}
}
+35
View File
@@ -0,0 +1,35 @@
// Package racename defines the Lobby Race Name Directory canonical-key
// policy and shared value types used by the platform-wide race-name
// uniqueness arbiter.
package racename
import (
"fmt"
"strings"
)
// CanonicalKey stores the policy-produced uniqueness key used to arbitrate
// race-name ownership across the Race Name Directory.
type CanonicalKey string
// String returns CanonicalKey as its stored canonical string.
func (key CanonicalKey) String() string {
return string(key)
}
// IsZero reports whether CanonicalKey does not contain a usable value.
func (key CanonicalKey) IsZero() bool {
return strings.TrimSpace(string(key)) == ""
}
// Validate reports whether CanonicalKey is non-empty and trimmed.
func (key CanonicalKey) Validate() error {
switch {
case key.IsZero():
return fmt.Errorf("race name canonical key must not be empty")
case strings.TrimSpace(string(key)) != string(key):
return fmt.Errorf("race name canonical key must not contain surrounding whitespace")
default:
return nil
}
}