Add client leaving host notification
This commit is contained in:
parent
7eeda3e6ae
commit
3c54a0f4d9
3 changed files with 164 additions and 0 deletions
|
|
@ -966,6 +966,23 @@ export const useGame = () => {
|
|||
showScoreboard();
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === 'LEAVE') {
|
||||
const playerId = conn.peer;
|
||||
const updatedPlayers = playersRef.current.filter(p => p.id !== playerId);
|
||||
playersRef.current = updatedPlayers;
|
||||
setPlayers(updatedPlayers);
|
||||
connectionsRef.current.delete(playerId);
|
||||
|
||||
if (presenterIdRef.current === playerId) {
|
||||
const realPlayers = updatedPlayers.filter(p => p.id !== 'host');
|
||||
const newPresenter = realPlayers.length > 0 ? realPlayers[0].id : null;
|
||||
setPresenterId(newPresenter);
|
||||
broadcast({ type: 'PRESENTER_CHANGED', payload: { presenterId: newPresenter } });
|
||||
}
|
||||
|
||||
broadcast({ type: 'PLAYER_LEFT', payload: { playerId } });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -1587,6 +1604,7 @@ export const useGame = () => {
|
|||
if (role !== 'CLIENT') return;
|
||||
|
||||
if (hostConnectionRef.current?.open) {
|
||||
hostConnectionRef.current.send({ type: 'LEAVE', payload: {} });
|
||||
hostConnectionRef.current.close();
|
||||
}
|
||||
hostConnectionRef.current = null;
|
||||
|
|
|
|||
|
|
@ -909,5 +909,150 @@ describe('Kick and Leave Feature Logic', () => {
|
|||
expect(isValidPlayerLeftMessage({ type: 'PLAYER_LEFT', payload: {} })).toBe(false);
|
||||
expect(isValidPlayerLeftMessage({ type: 'PLAYER_LEFT' })).toBe(false);
|
||||
});
|
||||
|
||||
it('validates LEAVE message structure', () => {
|
||||
const isValidLeaveMessage = (message: unknown): boolean => {
|
||||
if (typeof message !== 'object' || message === null) return false;
|
||||
const msg = message as { type?: string; payload?: object };
|
||||
return msg.type === 'LEAVE' && typeof msg.payload === 'object';
|
||||
};
|
||||
|
||||
expect(isValidLeaveMessage({ type: 'LEAVE', payload: {} })).toBe(true);
|
||||
expect(isValidLeaveMessage({ type: 'LEAVE' })).toBe(false);
|
||||
expect(isValidLeaveMessage({ type: 'OTHER', payload: {} })).toBe(false);
|
||||
expect(isValidLeaveMessage(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('host handling LEAVE message', () => {
|
||||
const processLeaveMessage = (
|
||||
leavingPlayerId: string,
|
||||
players: Player[],
|
||||
presenterId: string | null
|
||||
): {
|
||||
updatedPlayers: Player[];
|
||||
newPresenterId: string | null;
|
||||
shouldBroadcastPlayerLeft: boolean;
|
||||
shouldBroadcastPresenterChanged: boolean;
|
||||
} => {
|
||||
const updatedPlayers = players.filter(p => p.id !== leavingPlayerId);
|
||||
const wasPresenter = presenterId === leavingPlayerId;
|
||||
|
||||
let newPresenterId = presenterId;
|
||||
if (wasPresenter) {
|
||||
const realPlayers = updatedPlayers.filter(p => p.id !== 'host');
|
||||
newPresenterId = realPlayers.length > 0 ? realPlayers[0].id : null;
|
||||
}
|
||||
|
||||
return {
|
||||
updatedPlayers,
|
||||
newPresenterId,
|
||||
shouldBroadcastPlayerLeft: true,
|
||||
shouldBroadcastPresenterChanged: wasPresenter,
|
||||
};
|
||||
};
|
||||
|
||||
it('removes leaving player from players list', () => {
|
||||
const players = [
|
||||
createPlayer({ id: 'player-1', name: 'Alice' }),
|
||||
createPlayer({ id: 'player-2', name: 'Bob' }),
|
||||
];
|
||||
|
||||
const result = processLeaveMessage('player-1', players, null);
|
||||
|
||||
expect(result.updatedPlayers).toHaveLength(1);
|
||||
expect(result.updatedPlayers[0].id).toBe('player-2');
|
||||
});
|
||||
|
||||
it('broadcasts PLAYER_LEFT to remaining players', () => {
|
||||
const players = [
|
||||
createPlayer({ id: 'player-1', name: 'Alice' }),
|
||||
createPlayer({ id: 'player-2', name: 'Bob' }),
|
||||
];
|
||||
|
||||
const result = processLeaveMessage('player-1', players, null);
|
||||
|
||||
expect(result.shouldBroadcastPlayerLeft).toBe(true);
|
||||
});
|
||||
|
||||
it('reassigns presenter when leaving player was presenter', () => {
|
||||
const players = [
|
||||
createPlayer({ id: 'player-1', name: 'Alice' }),
|
||||
createPlayer({ id: 'player-2', name: 'Bob' }),
|
||||
];
|
||||
|
||||
const result = processLeaveMessage('player-1', players, 'player-1');
|
||||
|
||||
expect(result.newPresenterId).toBe('player-2');
|
||||
expect(result.shouldBroadcastPresenterChanged).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves presenter when non-presenter leaves', () => {
|
||||
const players = [
|
||||
createPlayer({ id: 'player-1', name: 'Alice' }),
|
||||
createPlayer({ id: 'player-2', name: 'Bob' }),
|
||||
];
|
||||
|
||||
const result = processLeaveMessage('player-2', players, 'player-1');
|
||||
|
||||
expect(result.newPresenterId).toBe('player-1');
|
||||
expect(result.shouldBroadcastPresenterChanged).toBe(false);
|
||||
});
|
||||
|
||||
it('sets presenter to null when last non-host player leaves', () => {
|
||||
const players = [
|
||||
createPlayer({ id: 'host', name: 'Host' }),
|
||||
createPlayer({ id: 'player-1', name: 'Alice' }),
|
||||
];
|
||||
|
||||
const result = processLeaveMessage('player-1', players, 'player-1');
|
||||
|
||||
expect(result.newPresenterId).toBeNull();
|
||||
expect(result.shouldBroadcastPresenterChanged).toBe(true);
|
||||
});
|
||||
|
||||
it('handles leave when player does not exist', () => {
|
||||
const players = [
|
||||
createPlayer({ id: 'player-1', name: 'Alice' }),
|
||||
];
|
||||
|
||||
const result = processLeaveMessage('player-999', players, null);
|
||||
|
||||
expect(result.updatedPlayers).toHaveLength(1);
|
||||
expect(result.shouldBroadcastPlayerLeft).toBe(true);
|
||||
});
|
||||
|
||||
it('does not modify original players array', () => {
|
||||
const players = [
|
||||
createPlayer({ id: 'player-1', name: 'Alice' }),
|
||||
createPlayer({ id: 'player-2', name: 'Bob' }),
|
||||
];
|
||||
const originalLength = players.length;
|
||||
|
||||
processLeaveMessage('player-1', players, null);
|
||||
|
||||
expect(players).toHaveLength(originalLength);
|
||||
});
|
||||
|
||||
it('handles multiple sequential leaves', () => {
|
||||
let players = [
|
||||
createPlayer({ id: 'player-1', name: 'Alice' }),
|
||||
createPlayer({ id: 'player-2', name: 'Bob' }),
|
||||
createPlayer({ id: 'player-3', name: 'Charlie' }),
|
||||
];
|
||||
let presenterId: string | null = 'player-1';
|
||||
|
||||
const result1 = processLeaveMessage('player-1', players, presenterId);
|
||||
players = result1.updatedPlayers;
|
||||
presenterId = result1.newPresenterId;
|
||||
|
||||
const result2 = processLeaveMessage('player-2', players, presenterId);
|
||||
players = result2.updatedPlayers;
|
||||
presenterId = result2.newPresenterId;
|
||||
|
||||
expect(players).toHaveLength(1);
|
||||
expect(players[0].id).toBe('player-3');
|
||||
expect(presenterId).toBe('player-3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
1
types.ts
1
types.ts
|
|
@ -229,4 +229,5 @@ export type NetworkMessage =
|
|||
| { type: 'ADVANCE'; payload: { action: 'START' | 'NEXT' | 'SCOREBOARD' } }
|
||||
| { type: 'KICK'; payload: { playerId: string } }
|
||||
| { type: 'KICKED'; payload: { reason?: string } }
|
||||
| { type: 'LEAVE'; payload: {} }
|
||||
| { type: 'PLAYER_LEFT'; payload: { playerId: string } };
|
||||
Loading…
Add table
Add a link
Reference in a new issue