diff --git a/pkg/lobby/README.md b/pkg/plugin/lobby/README.md similarity index 100% rename from pkg/lobby/README.md rename to pkg/plugin/lobby/README.md diff --git a/pkg/lobby/component/lobby.go b/pkg/plugin/lobby/component/lobby.go similarity index 94% rename from pkg/lobby/component/lobby.go rename to pkg/plugin/lobby/component/lobby.go index dbf2b2db6..37ee527fb 100644 --- a/pkg/lobby/component/lobby.go +++ b/pkg/plugin/lobby/component/lobby.go @@ -27,12 +27,22 @@ func (PlayerComponent) Name() string { return "player" } // Team represents a team within a lobby. type Team struct { + // TeamID is the stable identifier used to reference this team in all + // commands and events. Server-assigned at lobby creation from the + // preset registry and immutable afterward. TeamID string `json:"team_id"` - Name string `json:"name"` PlayerIDs []string `json:"player_ids"` // References to player IDs (source of truth) MaxPlayers int `json:"max_players"` } +// TeamConfig is a creation-time team specification used inside a lobby preset. +// Server operators declare presets in lobby.Config.LobbyPresets; clients choose +// a preset by name on CreateLobbyCommand. MaxPlayers <= 0 means unlimited. +type TeamConfig struct { + TeamID string `json:"team_id"` + MaxPlayers int `json:"max_players"` +} + // Session represents the current session state of a lobby. type Session struct { State SessionState `json:"state"` @@ -97,16 +107,6 @@ func (l *LobbyComponent) GetTeam(teamID string) *Team { return nil } -// GetTeamByName returns a team by its name. -func (l *LobbyComponent) GetTeamByName(name string) *Team { - for i := range l.Teams { - if l.Teams[i].Name == name { - return &l.Teams[i] - } - } - return nil -} - // HasPlayer returns true if the player is in any team. func (l *LobbyComponent) HasPlayer(playerID string) bool { for _, team := range l.Teams { @@ -428,6 +428,12 @@ type ConfigComponent struct { // start itself and returns to Idle. Values <= 0 disable timeout // enforcement entirely. MaxAllocationTimeout int64 `json:"max_allocation_timeout,omitempty"` + + // LobbyPresets is the server-owned registry of team configurations + // that clients can reference by label in CreateLobbyCommand.Preset. + // Server is the source of truth for team caps; clients cannot supply + // arbitrary TeamConfig values. + LobbyPresets map[string][]TeamConfig `json:"lobby_presets,omitempty"` } // Name returns the component name for ECS registration. diff --git a/pkg/lobby/component/lobby_internal_test.go b/pkg/plugin/lobby/component/lobby_internal_test.go similarity index 98% rename from pkg/lobby/component/lobby_internal_test.go rename to pkg/plugin/lobby/component/lobby_internal_test.go index efa1e6342..dfbfb3b53 100644 --- a/pkg/lobby/component/lobby_internal_test.go +++ b/pkg/plugin/lobby/component/lobby_internal_test.go @@ -39,14 +39,14 @@ func TestLobbyComponent_GetTeam(t *testing.T) { lobby := &LobbyComponent{ Teams: []Team{ - {TeamID: "team1", Name: "Team One"}, - {TeamID: "team2", Name: "Team Two"}, + {TeamID: "team1"}, + {TeamID: "team2"}, }, } team := lobby.GetTeam("team1") require.NotNil(t, team) - assert.Equal(t, "Team One", team.Name) + assert.Equal(t, "team1", team.TeamID) assert.Nil(t, lobby.GetTeam("unknown")) } diff --git a/pkg/lobby/lobby_test.go b/pkg/plugin/lobby/lobby_test.go similarity index 97% rename from pkg/lobby/lobby_test.go rename to pkg/plugin/lobby/lobby_test.go index ef7b48c8a..96d2763b1 100644 --- a/pkg/lobby/lobby_test.go +++ b/pkg/plugin/lobby/lobby_test.go @@ -5,7 +5,7 @@ import ( "github.com/argus-labs/world-engine/pkg/cardinal" "github.com/argus-labs/world-engine/pkg/cardinal/snapshot" - "github.com/argus-labs/world-engine/pkg/lobby" + "github.com/argus-labs/world-engine/pkg/plugin/lobby" "github.com/stretchr/testify/require" ) diff --git a/pkg/lobby/plugin.go b/pkg/plugin/lobby/plugin.go similarity index 91% rename from pkg/lobby/plugin.go rename to pkg/plugin/lobby/plugin.go index b14e3d8c5..5648256f1 100644 --- a/pkg/lobby/plugin.go +++ b/pkg/plugin/lobby/plugin.go @@ -30,8 +30,8 @@ package lobby import ( "github.com/argus-labs/world-engine/pkg/cardinal" - "github.com/argus-labs/world-engine/pkg/lobby/component" - "github.com/argus-labs/world-engine/pkg/lobby/system" + "github.com/argus-labs/world-engine/pkg/plugin/lobby/component" + "github.com/argus-labs/world-engine/pkg/plugin/lobby/system" ) // Re-export types for easier user access. @@ -63,6 +63,7 @@ type ( UpdatePlayerPassthroughCommand = system.UpdatePlayerPassthroughCommand GetPlayerCommand = system.GetPlayerCommand GetAllPlayersCommand = system.GetAllPlayersCommand + GetLobbyCommand = system.GetLobbyCommand // Events (Broadcast). CreatedEvent = system.LobbyCreatedEvent @@ -95,6 +96,7 @@ type ( UpdatePlayerPassthroughResult = system.UpdatePlayerPassthroughResult GetPlayerResult = system.GetPlayerResult GetAllPlayersResult = system.GetAllPlayersResult + GetLobbyResult = system.GetLobbyResult // Cross-Shard Commands. NotifySessionStartCommand = system.NotifySessionStartCommand @@ -139,6 +141,14 @@ type Config struct { // Clients should send heartbeats more frequently than this (e.g., every timeout/3 seconds). // Default: 30 seconds. HeartbeatTimeout int64 + + // LobbyPresets is the server-owned registry of team configurations + // that clients can reference by label in CreateLobbyCommand.Preset. + // The map key is the preset label; the value is the ordered list of + // teams that a lobby created with that preset will contain. Server + // operators are the source of truth for team caps; clients cannot + // supply arbitrary team configurations. + LobbyPresets map[string][]TeamConfig } // Plugin implements cardinal.Plugin for the lobby system. @@ -160,6 +170,7 @@ func (p *Plugin) Register(world *cardinal.World) { HeartbeatTimeout: p.config.HeartbeatTimeout, AssignmentAuthority: p.config.AssignmentAuthority, MaxAllocationTimeout: p.config.MaxAllocationTimeout, + LobbyPresets: p.config.LobbyPresets, }) // Store provider diff --git a/pkg/lobby/system/lobby.go b/pkg/plugin/lobby/system/lobby.go similarity index 92% rename from pkg/lobby/system/lobby.go rename to pkg/plugin/lobby/system/lobby.go index 63d52e1c4..d860c5014 100644 --- a/pkg/lobby/system/lobby.go +++ b/pkg/plugin/lobby/system/lobby.go @@ -7,7 +7,7 @@ import ( "time" "github.com/argus-labs/world-engine/pkg/cardinal" - "github.com/argus-labs/world-engine/pkg/lobby/component" + "github.com/argus-labs/world-engine/pkg/plugin/lobby/component" "github.com/google/uuid" ) @@ -16,10 +16,13 @@ import ( // ----------------------------------------------------------------------------- // CreateLobbyCommand creates a new lobby with the sender as leader. +// The server resolves Preset against lobby.Config.LobbyPresets and +// rejects unknown or empty preset labels. Clients cannot supply +// arbitrary team configuration; the server is the source of truth. type CreateLobbyCommand struct { RequestID string `json:"request_id"` // For matching request/response - // Teams is the initial team configuration for the lobby. - Teams []TeamConfig `json:"teams,omitempty"` + // Preset is the label of a server-registered team configuration. + Preset string `json:"preset"` // GameWorld is the target game shard address for this lobby. GameWorld cardinal.OtherWorld `json:"game_world"` // PlayerPassthroughData is custom data for the creating player, forwarded to game shard. @@ -28,20 +31,18 @@ type CreateLobbyCommand struct { SessionPassthroughData map[string]any `json:"session_passthrough_data,omitempty"` } -// TeamConfig defines initial team configuration. -type TeamConfig struct { - Name string `json:"name"` - MaxPlayers int `json:"max_players"` -} +// TeamConfig is an alias for component.TeamConfig to preserve the +// existing external type path (lobby.TeamConfig). +type TeamConfig = component.TeamConfig // Name returns the command name. func (CreateLobbyCommand) Name() string { return "lobby_create" } // JoinLobbyCommand joins an existing lobby via invite code. type JoinLobbyCommand struct { - RequestID string `json:"request_id"` // For matching request/response - InviteCode string `json:"invite_code"` // Required: invite code to join - TeamName string `json:"team_name,omitempty"` // Optional: team to join by name (joins first available if empty) + RequestID string `json:"request_id"` // For matching request/response + InviteCode string `json:"invite_code"` // Required: invite code to join + TeamID string `json:"team_id,omitempty"` // Optional: team to join by ID (joins first available if empty) // PlayerPassthroughData is custom data for the joining player, forwarded to game shard. PlayerPassthroughData map[string]any `json:"player_passthrough_data,omitempty"` } @@ -52,7 +53,7 @@ func (JoinLobbyCommand) Name() string { return "lobby_join" } // JoinTeamCommand moves a player to a different team. type JoinTeamCommand struct { RequestID string `json:"request_id"` // For matching request/response - TeamName string `json:"team_name"` + TeamID string `json:"team_id"` } // Name returns the command name. @@ -156,6 +157,16 @@ type GetAllPlayersCommand struct { // Name returns the command name. func (GetAllPlayersCommand) Name() string { return "lobby_get_all_players" } +// GetLobbyCommand fetches the caller's current lobby snapshot. Used by +// reconnecting clients to restore their full view (teams, session +// state, assigned GameWorld, etc.) in a single round-trip. +type GetLobbyCommand struct { + RequestID string `json:"request_id"` // For matching request/response +} + +// Name returns the command name. +func (GetLobbyCommand) Name() string { return "lobby_get_lobby" } + // ----------------------------------------------------------------------------- // Events (Broadcast) // ----------------------------------------------------------------------------- @@ -172,9 +183,9 @@ func (LobbyCreatedEvent) Name() string { return "lobby_created" } // PlayerJoinedEvent is emitted when a player joins a lobby. type PlayerJoinedEvent struct { - LobbyID string `json:"lobby_id"` - TeamName string `json:"team_name"` - Player component.PlayerComponent `json:"player"` + LobbyID string `json:"lobby_id"` + TeamID string `json:"team_id"` + Player component.PlayerComponent `json:"player"` } // Name returns the event name. @@ -210,10 +221,10 @@ func (PlayerReadyEvent) Name() string { return "lobby_player_ready" } // PlayerChangedTeamEvent is emitted when a player changes team. type PlayerChangedTeamEvent struct { - LobbyID string `json:"lobby_id"` - OldTeamName string `json:"old_team_name"` - NewTeamName string `json:"new_team_name"` - Player component.PlayerComponent `json:"player"` + LobbyID string `json:"lobby_id"` + OldTeamID string `json:"old_team_id"` + NewTeamID string `json:"new_team_id"` + Player component.PlayerComponent `json:"player"` } // Name returns the event name. @@ -229,9 +240,13 @@ type LeaderChangedEvent struct { // Name returns the event name. func (LeaderChangedEvent) Name() string { return "lobby_leader_changed" } -// SessionStartedEvent is emitted when a session starts. +// SessionStartedEvent is emitted when a session starts. Carries the +// assigned game shard address so every player in the lobby (not just +// the session starter) learns which gameplay shard to connect to +// without a follow-up query. type SessionStartedEvent struct { - LobbyID string `json:"lobby_id"` + LobbyID string `json:"lobby_id"` + GameWorld cardinal.OtherWorld `json:"game_world"` } // Name returns the event name. @@ -382,10 +397,13 @@ func (r TransferLeaderResult) Name() string { return r.RequestID + "_transfer_le // StartSessionResult is sent back to the client after StartSessionCommand. // Emitted asynchronously — may arrive several ticks after the command, // once the orchestrator assigns a game shard via AssignShardCommand. +// On success, GameWorld holds the assigned shard address so the client +// can connect without an extra query. Zero-valued on failure. type StartSessionResult struct { - RequestID string `json:"request_id"` - IsSuccess bool `json:"is_success"` - Message string `json:"message"` + RequestID string `json:"request_id"` + IsSuccess bool `json:"is_success"` + Message string `json:"message"` + GameWorld cardinal.OtherWorld `json:"game_world,omitempty"` } // Name returns the request-prefixed event name. @@ -453,6 +471,19 @@ func (r GetAllPlayersResult) Name() string { return r.RequestID + "_get_all_players_result" } +// GetLobbyResult is sent back to the client after GetLobbyCommand. +type GetLobbyResult struct { + RequestID string `json:"request_id"` + IsSuccess bool `json:"is_success"` + Message string `json:"message"` + Lobby component.LobbyComponent `json:"lobby,omitempty"` +} + +// Name returns the request-prefixed event name. +func (r GetLobbyResult) Name() string { + return r.RequestID + "_get_lobby_result" +} + // ----------------------------------------------------------------------------- // Cross-Shard Commands // ----------------------------------------------------------------------------- @@ -613,6 +644,7 @@ type LobbySystemState struct { UpdatePlayerPassthroughCmds cardinal.WithCommand[UpdatePlayerPassthroughCommand] GetPlayerCmds cardinal.WithCommand[GetPlayerCommand] GetAllPlayersCmds cardinal.WithCommand[GetAllPlayersCommand] + GetLobbyCmds cardinal.WithCommand[GetLobbyCommand] // Entities Lobbies cardinal.Contains[struct { @@ -661,6 +693,7 @@ type LobbySystemState struct { UpdatePlayerPassthroughResults cardinal.WithEvent[UpdatePlayerPassthroughResult] GetPlayerResults cardinal.WithEvent[GetPlayerResult] GetAllPlayersResults cardinal.WithEvent[GetAllPlayersResult] + GetLobbyResults cardinal.WithEvent[GetLobbyResult] } // lobbyLookupResult holds the result of looking up a player's lobby. @@ -730,7 +763,7 @@ func LobbySystem(state *LobbySystemState) { } // Process all commands - processCreateLobbyCommands(state, &lobbyIndex, now, timeout) + processCreateLobbyCommands(state, &lobbyIndex, &config, now, timeout) processJoinLobbyCommands(state, &lobbyIndex, now, timeout) processJoinTeamCommands(state, &lobbyIndex) processLeaveLobbyCommands(state, &lobbyIndex) @@ -746,6 +779,7 @@ func LobbySystem(state *LobbySystemState) { processUpdatePlayerPassthroughCommands(state, &lobbyIndex) processGetPlayerCommands(state, &lobbyIndex) processGetAllPlayersCommands(state, &lobbyIndex) + processGetLobbyCommands(state, &lobbyIndex) // Save lobby index if lobbyIndexEntity, err := state.LobbyIndexes.GetByID(lobbyIndexEntityID); err == nil { @@ -933,19 +967,41 @@ func processHeartbeatCommands( } } -// validateUniqueTeamNames checks for duplicate team names in config. -// Returns the duplicate name if found, empty string otherwise. -func validateUniqueTeamNames(teams []TeamConfig) string { - teamNames := make(map[string]bool) +// validateUniqueTeamIDs checks for duplicate team IDs in a preset. +// Returns the duplicate ID if found, empty string otherwise. +func validateUniqueTeamIDs(teams []TeamConfig) string { + teamIDs := make(map[string]bool) for _, tc := range teams { - if teamNames[tc.Name] { - return tc.Name + if teamIDs[tc.TeamID] { + return tc.TeamID } - teamNames[tc.Name] = true + teamIDs[tc.TeamID] = true } return "" } +// resolvePreset validates and looks up a preset in the server-owned +// registry. Returns the team list on success, or nil and an +// error message describing why the preset was rejected. The server is +// the source of truth for team configuration; the client's preset +// label must match an entry the operator registered. +func resolvePreset(preset string, presets map[string][]TeamConfig) ([]TeamConfig, string) { + if preset == "" { + return nil, "preset is required" + } + teams, ok := presets[preset] + if !ok { + return nil, "unknown preset: " + preset + } + if len(teams) == 0 { + return nil, "preset misconfigured: no teams" + } + if duplicateID := validateUniqueTeamIDs(teams); duplicateID != "" { + return nil, "preset misconfigured: duplicate team id " + duplicateID + } + return teams, "" +} + // generateInviteCodeWithRetry generates an invite code with collision check. // Retries up to maxRetries times if collision detected. // Returns the code and whether generation succeeded. @@ -1013,12 +1069,12 @@ func gatherLobbyPlayers( } // findTargetTeam finds the team for a player to join. -// If teamName is provided, it finds that specific team. +// If teamID is provided, it finds that specific team. // Otherwise, it finds the first team with available space. // Returns the team and an error message (empty string if successful). -func findTargetTeam(lobby *component.LobbyComponent, teamName string) (*component.Team, string) { - if teamName != "" { - team := lobby.GetTeamByName(teamName) +func findTargetTeam(lobby *component.LobbyComponent, teamID string) (*component.Team, string) { + if teamID != "" { + team := lobby.GetTeam(teamID) if team == nil { return nil, "team not found" } @@ -1040,6 +1096,7 @@ func findTargetTeam(lobby *component.LobbyComponent, teamName string) (*componen func processCreateLobbyCommands( state *LobbySystemState, lobbyIndex *component.LobbyIndexComponent, + config *component.ConfigComponent, now, timeout int64, ) { for cmd := range state.CreateLobbyCmds.Iter() { @@ -1073,33 +1130,24 @@ func processCreateLobbyCommands( CreatedAt: now, } - // Create teams from config or default single team - if len(payload.Teams) > 0 { - // Validate unique team names - if duplicateName := validateUniqueTeamNames(payload.Teams); duplicateName != "" { - state.Logger().Warn().Str("team_name", duplicateName).Msg("duplicate team name") - state.CreateLobbyResults.Emit(CreateLobbyResult{ - RequestID: payload.RequestID, - IsSuccess: false, - Message: "duplicate team name: " + duplicateName, - }) - continue - } - - for i, tc := range payload.Teams { - lobby.Teams = append(lobby.Teams, component.Team{ - TeamID: fmt.Sprintf("team_%d", i), - Name: tc.Name, - MaxPlayers: tc.MaxPlayers, - }) - } - } else { - // Default: single team with no limit - lobby.Teams = []component.Team{{ - TeamID: "default", - Name: "Default", - MaxPlayers: 0, - }} + presetTeams, errMsg := resolvePreset(payload.Preset, config.LobbyPresets) + if errMsg != "" { + state.Logger().Warn(). + Str("player_id", playerID). + Str("preset", payload.Preset). + Msg("create lobby rejected: " + errMsg) + state.CreateLobbyResults.Emit(CreateLobbyResult{ + RequestID: payload.RequestID, + IsSuccess: false, + Message: errMsg, + }) + continue + } + for _, tc := range presetTeams { + lobby.Teams = append(lobby.Teams, component.Team{ + TeamID: tc.TeamID, + MaxPlayers: tc.MaxPlayers, + }) } // Generate invite code with collision check @@ -1198,9 +1246,9 @@ func processJoinLobbyCommands( } // Find target team - targetTeam, errMsg := findTargetTeam(&lobby, payload.TeamName) + targetTeam, errMsg := findTargetTeam(&lobby, payload.TeamID) if targetTeam == nil { - state.Logger().Warn().Str("lobby_id", lobbyID).Str("team_name", payload.TeamName).Msg(errMsg) + state.Logger().Warn().Str("lobby_id", lobbyID).Str("team_id", payload.TeamID).Msg(errMsg) emitJoinLobbyFailure(state, payload.RequestID, errMsg) continue } @@ -1223,14 +1271,14 @@ func processJoinLobbyCommands( state.Logger().Info(). Str("lobby_id", lobbyID). Str("player_id", playerID). - Str("team_name", targetTeam.Name). + Str("team_id", targetTeam.TeamID). Msg("Player joined lobby") // Emit broadcast event state.PlayerJoinedEvents.Emit(PlayerJoinedEvent{ - LobbyID: lobbyID, - TeamName: targetTeam.Name, - Player: playerComp, + LobbyID: lobbyID, + TeamID: targetTeam.TeamID, + Player: playerComp, }) // Gather all players in the lobby for the result @@ -1284,12 +1332,12 @@ func processJoinTeamCommands(state *LobbySystemState, lobbyIndex *component.Lobb }) continue } - oldTeamName := oldTeam.Name + oldTeamID := oldTeam.TeamID - // Find target team by name - newTeam := lobby.GetTeamByName(payload.TeamName) + // Find target team by ID + newTeam := lobby.GetTeam(payload.TeamID) if newTeam == nil { - state.Logger().Warn().Str("lobby_id", lobbyID).Str("team_name", payload.TeamName).Msg("team not found") + state.Logger().Warn().Str("lobby_id", lobbyID).Str("team_id", payload.TeamID).Msg("team not found") state.JoinTeamResults.Emit(JoinTeamResult{ RequestID: payload.RequestID, IsSuccess: false, @@ -1326,16 +1374,16 @@ func processJoinTeamCommands(state *LobbySystemState, lobbyIndex *component.Lobb state.Logger().Info(). Str("lobby_id", lobbyID). Str("player_id", playerID). - Str("old_team", oldTeamName). - Str("new_team", newTeam.Name). + Str("old_team", oldTeamID). + Str("new_team", newTeam.TeamID). Msg("Player changed team") // Emit broadcast event state.PlayerChangedTeamEvents.Emit(PlayerChangedTeamEvent{ - LobbyID: lobbyID, - OldTeamName: oldTeamName, - NewTeamName: newTeam.Name, - Player: playerComp, + LobbyID: lobbyID, + OldTeamID: oldTeamID, + NewTeamID: newTeam.TeamID, + Player: playerComp, }) state.JoinTeamResults.Emit(JoinTeamResult{ @@ -1911,7 +1959,8 @@ func processAssignShardCommands( Msg("Session started (async assignment)") state.SessionStartedEvents.Emit(SessionStartedEvent{ - LobbyID: payload.LobbyID, + LobbyID: payload.LobbyID, + GameWorld: lobby.GameWorld, }) dispatchSessionStart(state, config, &lobby, payload.LobbyID) @@ -1920,6 +1969,7 @@ func processAssignShardCommands( RequestID: requestID, IsSuccess: true, Message: "session started", + GameWorld: lobby.GameWorld, }) } } @@ -2223,6 +2273,30 @@ func processGetPlayerCommands(state *LobbySystemState, lobbyIndex *component.Lob } } +func processGetLobbyCommands(state *LobbySystemState, lobbyIndex *component.LobbyIndexComponent) { + for cmd := range state.GetLobbyCmds.Iter() { + playerID := cmd.Persona + payload := cmd.Payload + + result := getPlayerLobby(playerID, lobbyIndex, &state.Lobbies) + if result == nil { + state.GetLobbyResults.Emit(GetLobbyResult{ + RequestID: payload.RequestID, + IsSuccess: false, + Message: "player not in a lobby", + }) + continue + } + + state.GetLobbyResults.Emit(GetLobbyResult{ + RequestID: payload.RequestID, + IsSuccess: true, + Message: "lobby found", + Lobby: result.lobby, + }) + } +} + func processGetAllPlayersCommands(state *LobbySystemState, lobbyIndex *component.LobbyIndexComponent) { for cmd := range state.GetAllPlayersCmds.Iter() { playerID := cmd.Persona diff --git a/pkg/lobby/system/lobby_internal_test.go b/pkg/plugin/lobby/system/lobby_internal_test.go similarity index 92% rename from pkg/lobby/system/lobby_internal_test.go rename to pkg/plugin/lobby/system/lobby_internal_test.go index 95d8b9c98..f5c9217db 100644 --- a/pkg/lobby/system/lobby_internal_test.go +++ b/pkg/plugin/lobby/system/lobby_internal_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/argus-labs/world-engine/pkg/cardinal" - "github.com/argus-labs/world-engine/pkg/lobby/component" + "github.com/argus-labs/world-engine/pkg/plugin/lobby/component" "github.com/stretchr/testify/assert" ) @@ -74,11 +74,11 @@ func TestTeamConfig(t *testing.T) { t.Parallel() config := TeamConfig{ - Name: "Team Alpha", + TeamID: "alpha", MaxPlayers: 5, } - assert.Equal(t, "Team Alpha", config.Name) + assert.Equal(t, "alpha", config.TeamID) assert.Equal(t, 5, config.MaxPlayers) } @@ -297,7 +297,6 @@ func TestNotifySessionStartCommand(t *testing.T) { Teams: []component.Team{ { TeamID: "team1", - Name: "Team Alpha", PlayerIDs: []string{"player1", "player2"}, }, }, @@ -344,10 +343,7 @@ func TestCreateLobbyCommand_WithGameWorld(t *testing.T) { cmd := CreateLobbyCommand{ RequestID: "req-123", - Teams: []TeamConfig{ - {Name: "Team 1", MaxPlayers: 4}, - {Name: "Team 2", MaxPlayers: 4}, - }, + Preset: "2v2", GameWorld: cardinal.OtherWorld{ Region: "us-west", Organization: "myorg", @@ -357,7 +353,7 @@ func TestCreateLobbyCommand_WithGameWorld(t *testing.T) { } assert.Equal(t, "req-123", cmd.RequestID) - assert.Len(t, cmd.Teams, 2) + assert.Equal(t, "2v2", cmd.Preset) assert.Equal(t, "game-shard-1", cmd.GameWorld.ShardID) assert.Equal(t, "lobby_create", cmd.Name()) } @@ -563,11 +559,11 @@ func TestEventsWithPlayerComponent(t *testing.T) { // Test PlayerJoinedEvent includes Player joinedEvent := PlayerJoinedEvent{ - TeamName: "Team Alpha", - Player: player, + TeamID: "alpha", + Player: player, } assert.Equal(t, "player-123", joinedEvent.Player.PlayerID) - assert.Equal(t, "Team Alpha", joinedEvent.TeamName) + assert.Equal(t, "alpha", joinedEvent.TeamID) // Test PlayerReadyEvent includes Player readyEvent := PlayerReadyEvent{ @@ -578,13 +574,13 @@ func TestEventsWithPlayerComponent(t *testing.T) { // Test PlayerChangedTeamEvent includes Player changedTeamEvent := PlayerChangedTeamEvent{ - OldTeamName: "Team Alpha", - NewTeamName: "Team Beta", - Player: player, + OldTeamID: "alpha", + NewTeamID: "beta", + Player: player, } assert.Equal(t, "player-123", changedTeamEvent.Player.PlayerID) - assert.Equal(t, "Team Alpha", changedTeamEvent.OldTeamName) - assert.Equal(t, "Team Beta", changedTeamEvent.NewTeamName) + assert.Equal(t, "alpha", changedTeamEvent.OldTeamID) + assert.Equal(t, "beta", changedTeamEvent.NewTeamID) // Test PlayerPassthroughUpdatedEvent includes Player passthroughEvent := PlayerPassthroughUpdatedEvent{ @@ -599,40 +595,40 @@ func TestFindTargetTeam(t *testing.T) { lobby := &component.LobbyComponent{ Teams: []component.Team{ - {TeamID: "team1", Name: "Alpha", MaxPlayers: 2, PlayerIDs: []string{"p1", "p2"}}, // full - {TeamID: "team2", Name: "Beta", MaxPlayers: 2, PlayerIDs: []string{"p3"}}, // has space - {TeamID: "team3", Name: "Gamma", MaxPlayers: 0, PlayerIDs: []string{}}, // unlimited + {TeamID: "alpha", MaxPlayers: 2, PlayerIDs: []string{"p1", "p2"}}, // full + {TeamID: "beta", MaxPlayers: 2, PlayerIDs: []string{"p3"}}, // has space + {TeamID: "gamma", MaxPlayers: 0, PlayerIDs: []string{}}, // unlimited }, } tests := []struct { name string - teamName string + teamID string wantTeamID string wantErrMsg string }{ { - name: "find by name - exists with space", - teamName: "Beta", - wantTeamID: "team2", + name: "find by id - exists with space", + teamID: "beta", + wantTeamID: "beta", wantErrMsg: "", }, { - name: "find by name - team not found", - teamName: "NonExistent", + name: "find by id - team not found", + teamID: "nonexistent", wantTeamID: "", wantErrMsg: "team not found", }, { - name: "find by name - team is full", - teamName: "Alpha", + name: "find by id - team is full", + teamID: "alpha", wantTeamID: "", wantErrMsg: "team is full", }, { name: "auto-assign - finds first with space", - teamName: "", - wantTeamID: "team2", + teamID: "", + wantTeamID: "beta", wantErrMsg: "", }, } @@ -640,7 +636,7 @@ func TestFindTargetTeam(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - team, errMsg := findTargetTeam(lobby, tt.teamName) + team, errMsg := findTargetTeam(lobby, tt.teamID) if tt.wantErrMsg != "" { assert.Nil(t, team) assert.Equal(t, tt.wantErrMsg, errMsg) @@ -658,8 +654,8 @@ func TestFindTargetTeam_AllTeamsFull(t *testing.T) { lobby := &component.LobbyComponent{ Teams: []component.Team{ - {TeamID: "team1", Name: "Alpha", MaxPlayers: 1, PlayerIDs: []string{"p1"}}, - {TeamID: "team2", Name: "Beta", MaxPlayers: 1, PlayerIDs: []string{"p2"}}, + {TeamID: "alpha", MaxPlayers: 1, PlayerIDs: []string{"p1"}}, + {TeamID: "beta", MaxPlayers: 1, PlayerIDs: []string{"p2"}}, }, }