Skip to content

Commit c0e2528

Browse files
committed
Add TurnOrder to handle player join/leave and turn rotation
1 parent b620839 commit c0e2528

18 files changed

Lines changed: 326 additions & 138 deletions

File tree

packages/common/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type { RequestManager } from './requestManager';
1414
export { Response } from './response';
1515
export type { ResponseBuilder } from './response';
1616
export { RoomStateChangePayload } from './RoomStateChangePayload';
17+
export { newTag } from './tag';
1718
export { createArgEvent, createEvent } from './typedEvent';
1819
export type { TypedEvent, TypedEventWithArg } from './typedEvent';
1920
export { typeRegistry } from './typeRegistry';

packages/common/src/tag.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export function newTag<TargetT extends object, MetadataT>(
2+
name: string,
3+
): [(target: TargetT, value: MetadataT) => void, (target: TargetT) => MetadataT | undefined] {
4+
const registry = new WeakMap<TargetT, MetadataT>();
5+
return [
6+
(target, value) => {
7+
registry.set(target, value);
8+
},
9+
registry.get.bind(registry),
10+
];
11+
}

packages/server/src/ambients.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export const [getPlayerAmbient, provideGetPlayerAmbient] =
1010
/**
1111
* Player in a room
1212
*/
13-
export const [playersAmbient, providePlayersAmbient] = newAmbient<{ current: Player[] }>('players');
1413
export const [roomNameAmbient, provideRoomNameAmbient] = newAmbient<string>('room-name');
1514
export const [roomIdAmbient, provideRoomIdAmbient] = newAmbient<RoomId>('room-id');
1615
export const [roomTypeAmbient, provideRoomTypeAmbient] = newAmbient<RoomType>('room-type');

packages/server/src/demo.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { endState } from '@prisel/state';
12
import { debug, Server } from './index';
23

3-
const close = Server.create();
4+
const close = Server.create({
5+
onCreateGame: () => () => endState(),
6+
});
47
process.stdout.write('starting server');
58

69
process.title = 'prisel-server';

packages/server/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ export * from './objects/index';
1010
export { newPlayer } from './player';
1111
export type { Player, PlayerId, PlayerOption } from './player';
1212
export { newRoomEvent, playerJoinEvent, playerLeaveEvent } from './roomEvent';
13-
export type { CreateGame, RoomType } from './serverConfig';
13+
export { RoomType } from './serverConfig';
14+
export type { CreateGame } from './serverConfig';
1415
export { Server } from './serverState';
16+
export { RoundRobin } from './turnOrder';
17+
export type { TurnOrder } from './turnOrder';
1518
export { broadcast } from './utils/broadcast';
1619
export { useEventHandler } from './utils/useEventHandler';
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export interface CreateGameStatus {
22
message: string;
3-
detail: string;
3+
detail?: string;
44
}

packages/server/src/roomState.ts

Lines changed: 72 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import {
1313
useStored,
1414
} from '@prisel/state';
1515
import {
16-
playersAmbient,
17-
providePlayersAmbient,
1816
provideRoomNameAmbient,
1917
roomIdAmbient,
2018
roomNameAmbient,
@@ -23,7 +21,8 @@ import {
2321
import { isInRoom, isSystemAction, playerExitRoomEvent, systemActionRequestEvent } from './events';
2422
import { Player } from './player';
2523
import { emitPlayerJoinEvent, emitPlayerLeaveEvent } from './roomEvent';
26-
import { CreateGame, RoomType } from './serverConfig';
24+
import { CreateGame, CreateTurnOrder, RoomType } from './serverConfig';
25+
import { RoundRobin, TurnOrder } from './turnOrder';
2726
import { broadcast } from './utils/broadcast';
2827
import { pipe } from './utils/pipe';
2928
import { getPlayerInfo, getRoomStateSnapshot2 } from './utils/stateUtils';
@@ -64,30 +63,34 @@ const [roomStateToken, provideRoomStateToken] = newAmbient<{ current: number }>(
6463

6564
export function RoomState(props: {
6665
onCreateGame: CreateGame;
67-
createEvent?: { player: Player; request: Request };
66+
createRoomEvent?: { player: Player; request: Request };
67+
onCreateTurnOrder?: CreateTurnOrder;
6868
}): StateFuncReturn {
69-
const players = useStored<Player[]>([]); // TODO: players will change
7069
const roomStateToken = useStored(0);
7170

72-
const { onCreateGame, createEvent } = props;
71+
const {
72+
onCreateGame,
73+
createRoomEvent: createEvent,
74+
onCreateTurnOrder = (players) => new RoundRobin(players),
75+
} = props;
7376

7477
useSideEffect(() => {
75-
const inspector = run(
78+
const turnOrder = onCreateTurnOrder([]);
79+
run(
7680
pipe(
77-
newState(PreoccupiedRoomState, { onCreateGame, createEvent }),
78-
providePlayersAmbient(players),
81+
newState(PreoccupiedRoomState, { onCreateGame, createEvent, turnOrder }),
7982
provideRoomStateToken(roomStateToken),
8083
),
8184
);
82-
return inspector.exit;
8385
}, []);
8486
}
8587

8688
export function PreoccupiedRoomState(props: {
8789
onCreateGame: CreateGame;
8890
createEvent?: { player: Player; request: Request };
91+
turnOrder: TurnOrder;
8992
}): StateFuncReturn {
90-
const { onCreateGame, createEvent } = props;
93+
const { onCreateGame, createEvent, turnOrder } = props;
9194
const [roomName, setRoomName] = useLocalState('room');
9295
const [host, setHost] = useLocalState<Player>();
9396
const [occupied, setOccupied] = useLocalState(false);
@@ -109,7 +112,7 @@ export function PreoccupiedRoomState(props: {
109112
}
110113
const { roomName } = payload;
111114
setRoomName(roomName);
112-
addPlayer(player);
115+
addPlayer(turnOrder, player);
113116
setOccupied(true);
114117
setHost(player);
115118

@@ -121,7 +124,7 @@ export function PreoccupiedRoomState(props: {
121124
id: getAmbient(roomIdAmbient),
122125
},
123126
roomState: getRoomStateSnapshot2(
124-
getAmbient(playersAmbient).current,
127+
turnOrder.getAllPlayers(),
125128
updateRoomStateToken(),
126129
player.getId(),
127130
),
@@ -137,7 +140,7 @@ export function PreoccupiedRoomState(props: {
137140
player.respond(Response.forRequest(request).setFailure('No payload').build());
138141
return;
139142
}
140-
addPlayer(player);
143+
addPlayer(turnOrder, player);
141144
setOccupied(true);
142145
setHost(player);
143146
const previousToken = getRoomStateToken();
@@ -150,15 +153,15 @@ export function PreoccupiedRoomState(props: {
150153
id,
151154
},
152155
roomState: getRoomStateSnapshot2(
153-
getAmbient(playersAmbient).current,
156+
turnOrder.getAllPlayers(),
154157
token,
155158
player.getId(),
156159
),
157160
})
158161
.build(),
159162
);
160163
broadcast(
161-
getAmbient(playersAmbient).current,
164+
turnOrder.getAllPlayers(),
162165
Packet.forSystemAction(priselpb.SystemActionType.ROOM_STATE_CHANGE)
163166
.setPayload('roomStateChangePayload', {
164167
change: {
@@ -177,13 +180,17 @@ export function PreoccupiedRoomState(props: {
177180
if (occupied && host) {
178181
return provideRoomNameAmbient(
179182
roomName,
180-
newState(OccupiedRoomState, { host, onCreateGame }),
183+
newState(OccupiedRoomState, { host, onCreateGame, turnOrder }),
181184
);
182185
}
183186
}
184187

185-
function OccupiedRoomState(props: { host: Player; onCreateGame: CreateGame }): StateFuncReturn {
186-
const { onCreateGame } = props;
188+
function OccupiedRoomState(props: {
189+
host: Player;
190+
onCreateGame: CreateGame;
191+
turnOrder: TurnOrder;
192+
}): StateFuncReturn {
193+
const { onCreateGame, turnOrder } = props;
187194
const [host, setHost] = useLocalState(props.host);
188195
const [gameStarted, setGameStarted] = useLocalState(false);
189196
const [allPlayerLeft, setAllPlayerLeft] = useLocalState(false);
@@ -197,7 +204,7 @@ function OccupiedRoomState(props: { host: Player; onCreateGame: CreateGame }): S
197204
player.respond(Response.forRequest(request).setFailure('No payload').build());
198205
return;
199206
}
200-
addPlayer(player);
207+
addPlayer(turnOrder, player);
201208
const previousToken = getRoomStateToken();
202209
const token = updateRoomStateToken();
203210
player.respond(
@@ -208,15 +215,15 @@ function OccupiedRoomState(props: { host: Player; onCreateGame: CreateGame }): S
208215
id: getAmbient(roomIdAmbient),
209216
},
210217
roomState: getRoomStateSnapshot2(
211-
getAmbient(playersAmbient).current,
218+
turnOrder.getAllPlayers(),
212219
token,
213220
host.getId(),
214221
),
215222
})
216223
.build(),
217224
);
218225
broadcast(
219-
getAmbient(playersAmbient).current,
226+
turnOrder.getAllPlayers(),
220227
Packet.forSystemAction(priselpb.SystemActionType.ROOM_STATE_CHANGE)
221228
.setPayload('roomStateChangePayload', {
222229
change: {
@@ -247,11 +254,11 @@ function OccupiedRoomState(props: { host: Player; onCreateGame: CreateGame }): S
247254
return;
248255
}
249256

250-
const createGameResult = onCreateGame({ players: getAmbient(playersAmbient) });
257+
const createGameResult = onCreateGame({ turnOrder });
251258
if (typeof createGameResult === 'function') {
252259
player.respond(Response.forRequest(request).build());
253260
broadcast(
254-
getAmbient(playersAmbient).current,
261+
turnOrder.getAllPlayers(),
255262
Packet.forSystemAction(priselpb.SystemActionType.ANNOUNCE_GAME_START).build(),
256263
);
257264
run(createGameResult).onComplete(() => {
@@ -268,29 +275,44 @@ function OccupiedRoomState(props: { host: Player; onCreateGame: CreateGame }): S
268275
});
269276

270277
useEventHandler(leaveRequestEvent, ({ player, packet: request }) => {
271-
removePlayer(player);
278+
removePlayer(turnOrder, player);
272279
player.respond(Response.forRequest(request).build());
273-
updateRoomStateAfterPlayerLeave(player, host, setHost, setAllPlayerLeft);
280+
updateRoomStateAfterPlayerLeave(
281+
player,
282+
player.equals(host),
283+
turnOrder,
284+
setHost,
285+
setAllPlayerLeft,
286+
);
274287
});
275288
// if player disconnect without leaving the room first
276289
useEventHandler(playerExitRoomEvent, (player) => {
277-
removePlayer(player);
278-
updateRoomStateAfterPlayerLeave(player, host, setHost, setAllPlayerLeft);
290+
removePlayer(turnOrder, player);
291+
updateRoomStateAfterPlayerLeave(
292+
player,
293+
player.equals(host),
294+
turnOrder,
295+
setHost,
296+
setAllPlayerLeft,
297+
);
279298
});
280299
if (allPlayerLeft) {
281300
switch (getAmbient(roomTypeAmbient)) {
282301
case RoomType.DEFAULT:
283302
// single room mode, we will allow reentrance
284-
return newState(AllPlayerLeftRoomState, { onCreateGame });
303+
return newState(AllPlayerLeftRoomState, { onCreateGame, turnOrder });
285304
case RoomType.MULTI:
286305
// multi room mode, we will end the room
287306
return endState();
288307
}
289308
}
290309
}
291310

292-
export function AllPlayerLeftRoomState(props: { onCreateGame: CreateGame }): StateFuncReturn {
293-
const { onCreateGame } = props;
311+
export function AllPlayerLeftRoomState(props: {
312+
onCreateGame: CreateGame;
313+
turnOrder: TurnOrder;
314+
}): StateFuncReturn {
315+
const { onCreateGame, turnOrder } = props;
294316
const [host, setHost] = useLocalState<Player>();
295317
const [occupied, setOccupied] = useLocalState(false);
296318
const id = getAmbient(roomIdAmbient);
@@ -301,7 +323,7 @@ export function AllPlayerLeftRoomState(props: { onCreateGame: CreateGame }): Sta
301323
player.respond(Response.forRequest(request).setFailure('No payload').build());
302324
return;
303325
}
304-
addPlayer(player);
326+
addPlayer(turnOrder, player);
305327
setOccupied(true);
306328
setHost(player);
307329
const previousToken = getRoomStateToken();
@@ -314,15 +336,15 @@ export function AllPlayerLeftRoomState(props: { onCreateGame: CreateGame }): Sta
314336
id: getAmbient(roomIdAmbient),
315337
},
316338
roomState: getRoomStateSnapshot2(
317-
getAmbient(playersAmbient).current,
339+
turnOrder.getAllPlayers(),
318340
token,
319341
player.getId(),
320342
),
321343
})
322344
.build(),
323345
);
324346
broadcast(
325-
getAmbient(playersAmbient).current,
347+
turnOrder.getAllPlayers(),
326348
Packet.forSystemAction(priselpb.SystemActionType.ROOM_STATE_CHANGE)
327349
.setPayload('roomStateChangePayload', {
328350
change: {
@@ -339,30 +361,30 @@ export function AllPlayerLeftRoomState(props: { onCreateGame: CreateGame }): Sta
339361
});
340362

341363
if (occupied && host) {
342-
return newState(OccupiedRoomState, { host, onCreateGame });
364+
return newState(OccupiedRoomState, { host, onCreateGame, turnOrder });
343365
}
344366
}
345367

346368
function updateRoomStateAfterPlayerLeave(
347369
player: Player,
348-
host: Player,
370+
isHostLeft: boolean,
371+
turnOrder: TurnOrder,
349372
setHost: SetLocalState<Player>,
350373
setAllPlayerLeft: SetLocalState<boolean>,
351374
) {
352-
const players = getAmbient(playersAmbient).current;
353-
if (players.length === 0) {
375+
if (turnOrder.size === 0) {
354376
// all player left.
355377
setAllPlayerLeft(true);
356378
return;
357379
}
358380
const previousToken = getRoomStateToken();
359381
const token = updateRoomStateToken();
360-
if (player.equals(host)) {
361-
// host left. We will replace host
362-
const newHost = players[0];
382+
if (isHostLeft) {
383+
// host left. We will choose the current player as host
384+
const newHost = turnOrder.getCurrentPlayer()!!;
363385
setHost(newHost);
364386
broadcast(
365-
players,
387+
turnOrder.getAllPlayers(),
366388
Packet.forSystemAction(priselpb.SystemActionType.ROOM_STATE_CHANGE)
367389
.setPayload('roomStateChangePayload', {
368390
change: {
@@ -383,7 +405,7 @@ function updateRoomStateAfterPlayerLeave(
383405
}
384406
// player left
385407
broadcast(
386-
players,
408+
turnOrder.getAllPlayers(),
387409
Packet.forSystemAction(priselpb.SystemActionType.ROOM_STATE_CHANGE)
388410
.setPayload('roomStateChangePayload', {
389411
change: {
@@ -409,14 +431,15 @@ function updateRoomStateToken(): string {
409431
return `${token.current}`;
410432
}
411433

412-
function addPlayer(player: Player) {
413-
const players = getAmbient(playersAmbient);
434+
function addPlayer(turnOrder: TurnOrder, player: Player) {
414435
player.setRoomId(getAmbient(roomIdAmbient));
415-
players.current.push(player);
436+
turnOrder.addPlayer(player);
416437
}
417438

418-
function removePlayer(player: Player) {
419-
const players = getAmbient(playersAmbient);
439+
function removePlayer(turnOrder: TurnOrder, player: Player) {
420440
player.clearRoomId();
421-
players.current = players.current.filter((target) => !target.equals(player));
441+
if (player.equals(turnOrder.getCurrentPlayer())) {
442+
turnOrder.giveTurnToNext();
443+
}
444+
turnOrder.removePlayer(player);
422445
}

0 commit comments

Comments
 (0)