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();
|
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(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1587,6 +1604,7 @@ export const useGame = () => {
|
||||||
if (role !== 'CLIENT') return;
|
if (role !== 'CLIENT') return;
|
||||||
|
|
||||||
if (hostConnectionRef.current?.open) {
|
if (hostConnectionRef.current?.open) {
|
||||||
|
hostConnectionRef.current.send({ type: 'LEAVE', payload: {} });
|
||||||
hostConnectionRef.current.close();
|
hostConnectionRef.current.close();
|
||||||
}
|
}
|
||||||
hostConnectionRef.current = null;
|
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', payload: {} })).toBe(false);
|
||||||
expect(isValidPlayerLeftMessage({ type: 'PLAYER_LEFT' })).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: 'ADVANCE'; payload: { action: 'START' | 'NEXT' | 'SCOREBOARD' } }
|
||||||
| { type: 'KICK'; payload: { playerId: string } }
|
| { type: 'KICK'; payload: { playerId: string } }
|
||||||
| { type: 'KICKED'; payload: { reason?: string } }
|
| { type: 'KICKED'; payload: { reason?: string } }
|
||||||
|
| { type: 'LEAVE'; payload: {} }
|
||||||
| { type: 'PLAYER_LEFT'; payload: { playerId: string } };
|
| { type: 'PLAYER_LEFT'; payload: { playerId: string } };
|
||||||
Loading…
Add table
Add a link
Reference in a new issue