Private Lobbies: Add kick player functionality (#1436)

## Description:
Added player management features so lobby hosts can kick players from
private games. This includes both UI changes and backend work.

### What's new:
- Hosts can now kick players from private lobbies with a simple button
- Added host badges and remove buttons to the UI
- Made sure only hosts can kick people, and hosts can't kick themselves

### How it works:
- When someone creates a private game, they automatically become the
host
- Kicking happens through WebSocket "kick-player" events
- Server checks that you're actually the host before letting you kick
anyone

<img width="1291" height="871" alt="Screenshot 2025-07-15 002114"
src="https://github.com/user-attachments/assets/ea575f83-a0f4-45d1-9cfe-7521d373f3d5"
/>



### Known Issues:
- Kicked player gets general message (same when kicked for multi tab)

### Other Issues:
- Host abandoment still existent (host clicks on x; or is closing tab)

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I have read and accepted the CLA agreement (only required once).

## Please put your Discord username so you can be contacted if a bug or
regression is found:

[UN]nvm

---------

Co-authored-by: floriankilian <floriankilian@users.noreply.github.com>
This commit is contained in:
floriankilian
2025-08-01 07:01:10 +02:00
committed by GitHub
parent ee459b7410
commit bd59cd61cb
10 changed files with 211 additions and 54 deletions
+50 -1
View File
@@ -41,7 +41,7 @@ export class GameServer {
private turns: Turn[] = [];
private intents: Intent[] = [];
public activeClients: Client[] = [];
// Used for record record keeping
private LobbyCreatorID: string | undefined;
private allClients: Map<ClientID, Client> = new Map();
private clientsDisconnectedStatus: Map<ClientID, boolean> = new Map();
private _hasStarted = false;
@@ -71,8 +71,10 @@ export class GameServer {
public readonly createdAt: number,
private config: ServerConfig,
public gameConfig: GameConfig,
lobbyCreatorID?: string,
) {
this.log = log_.child({ gameID: id });
this.LobbyCreatorID = lobbyCreatorID ?? undefined;
}
public updateGameConfig(gameConfig: Partial<GameConfig>): void {
@@ -118,6 +120,13 @@ export class GameServer {
});
return;
}
// Log when lobby creator joins private game
if (client.clientID === this.LobbyCreatorID) {
this.log.info("Lobby creator joined", {
gameID: this.id,
creatorID: this.LobbyCreatorID,
});
}
this.log.info("client (re)joining game", {
clientID: client.clientID,
persistentID: client.persistentID,
@@ -223,6 +232,42 @@ export class GameServer {
);
return;
}
// Handle kick_player intent via WebSocket
if (clientMsg.intent.type === "kick_player") {
const authenticatedClientID = client.clientID;
// Check if the authenticated client is the lobby creator
if (authenticatedClientID !== this.LobbyCreatorID) {
this.log.warn(`Only lobby creator can kick players`, {
clientID: authenticatedClientID,
creatorID: this.LobbyCreatorID,
target: clientMsg.intent.target,
gameID: this.id,
});
return;
}
// Don't allow lobby creator to kick themselves
if (authenticatedClientID === clientMsg.intent.target) {
this.log.warn(`Cannot kick yourself`, {
clientID: authenticatedClientID,
});
return;
}
// Log and execute the kick
this.log.info(`Lobby creator initiated kick of player`, {
creatorID: authenticatedClientID,
target: clientMsg.intent.target,
gameID: this.id,
kickMethod: "websocket",
});
this.kickClient(clientMsg.intent.target);
return;
}
this.addIntent(clientMsg.intent);
}
if (clientMsg.type === "ping") {
@@ -453,6 +498,10 @@ export class GameServer {
}
}
public isPrivateLobbyCreator(clientID: string): boolean {
return this.LobbyCreatorID === clientID;
}
phase(): GamePhase {
const now = Date.now();
const alive: Client[] = [];