feat: gamemaster
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
// Package playermapping defines the durable mapping between platform
|
||||
// users and engine player handles owned by Game Master.
|
||||
//
|
||||
// One PlayerMapping mirrors one row of the `player_mappings` PostgreSQL
|
||||
// table (see
|
||||
// `galaxy/gamemaster/internal/adapters/postgres/migrations/00001_init.sql`).
|
||||
// The composite primary key `(game_id, user_id)` and the unique
|
||||
// `(game_id, race_name)` index live in the SQL schema; the domain model
|
||||
// captures the per-row invariants enforced from the application side.
|
||||
package playermapping
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PlayerMapping stores one (game_id, user_id) → (race_name,
|
||||
// engine_player_uuid) projection installed at register-runtime.
|
||||
type PlayerMapping struct {
|
||||
// GameID identifies the game owning this mapping.
|
||||
GameID string
|
||||
|
||||
// UserID identifies the platform user this mapping refers to.
|
||||
UserID string
|
||||
|
||||
// RaceName stores the in-game race name reserved for the user in
|
||||
// the original casing presented by the engine.
|
||||
RaceName string
|
||||
|
||||
// EnginePlayerUUID stores the engine-side player handle returned by
|
||||
// the engine /admin/init response.
|
||||
EnginePlayerUUID string
|
||||
|
||||
// CreatedAt stores the wall-clock at which the row was inserted.
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether mapping satisfies the player-mapping
|
||||
// invariants implied by the README §Persistence Layout / player_mappings
|
||||
// columns and the SQL primary-key + unique-index constraints.
|
||||
func (mapping PlayerMapping) Validate() error {
|
||||
if strings.TrimSpace(mapping.GameID) == "" {
|
||||
return fmt.Errorf("game id must not be empty")
|
||||
}
|
||||
if strings.TrimSpace(mapping.UserID) == "" {
|
||||
return fmt.Errorf("user id must not be empty")
|
||||
}
|
||||
if strings.TrimSpace(mapping.RaceName) == "" {
|
||||
return fmt.Errorf("race name must not be empty")
|
||||
}
|
||||
if strings.TrimSpace(mapping.EnginePlayerUUID) == "" {
|
||||
return fmt.Errorf("engine player uuid must not be empty")
|
||||
}
|
||||
if mapping.CreatedAt.IsZero() {
|
||||
return fmt.Errorf("created at must not be zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrNotFound reports that a player-mapping lookup failed because no
|
||||
// matching row exists.
|
||||
var ErrNotFound = errors.New("player mapping not found")
|
||||
|
||||
// ErrConflict reports that a player-mapping insert could not be applied
|
||||
// because a row with the same `(game_id, user_id)` primary key or with
|
||||
// the same `(game_id, race_name)` unique pair already exists. Adapters
|
||||
// surface PostgreSQL unique-violations through this sentinel so the
|
||||
// service layer maps it to a `conflict` REST envelope.
|
||||
var ErrConflict = errors.New("player mapping already exists")
|
||||
@@ -0,0 +1,44 @@
|
||||
package playermapping
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func validMapping() PlayerMapping {
|
||||
return PlayerMapping{
|
||||
GameID: "game-1",
|
||||
UserID: "user-1",
|
||||
RaceName: "Aelinari",
|
||||
EnginePlayerUUID: "00000000-0000-0000-0000-000000000001",
|
||||
CreatedAt: time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC),
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerMappingValidateHappy(t *testing.T) {
|
||||
require.NoError(t, validMapping().Validate())
|
||||
}
|
||||
|
||||
func TestPlayerMappingValidateRejects(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*PlayerMapping)
|
||||
}{
|
||||
{"empty game id", func(m *PlayerMapping) { m.GameID = "" }},
|
||||
{"empty user id", func(m *PlayerMapping) { m.UserID = "" }},
|
||||
{"empty race name", func(m *PlayerMapping) { m.RaceName = "" }},
|
||||
{"empty engine uuid", func(m *PlayerMapping) { m.EnginePlayerUUID = "" }},
|
||||
{"zero created at", func(m *PlayerMapping) { m.CreatedAt = time.Time{} }},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mapping := validMapping()
|
||||
tt.mutate(&mapping)
|
||||
assert.Error(t, mapping.Validate())
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user