-
Notifications
You must be signed in to change notification settings - Fork 1
/
ServerGame.ts
191 lines (170 loc) · 6.27 KB
/
ServerGame.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import type WebSocket from "ws";
import { JsUtils } from ":defs/JsUtils";
import { SOCKET_ID, GroupEv, GameEv } from ":defs/OnlineDefs";
import type { Game } from ":game/Game";
import type { Coord } from ":floor/Tile";
import type { StateChange } from ":game/StateChange";
import { Player } from ":game/player/Player";
import { GetRobotImpl } from ":game/player/robot/ImplBarrel";
import { GetGridImpl } from ":floor/ImplBarrel";
import { GameManager } from ":game/gameparts/GameManager";
/** @this ServerGame */
function gameOnSocketMessage<S extends Coord.System>(this: ServerGame<S>, ev: WebSocket.MessageEvent): void {
const [evName, ...args] = JSON.parse(ev.data as string) as [string, ...any[]];
const socket = ev.target;
switch (evName) {
case GameEv.IN_GAME: this.requestStateChange(args[0], socket); break;
case GameEv.PAUSE: this.statusBecomePaused(); break;
case GameEv.UNPAUSE: this.statusBecomePlaying(); break;
case GameEv.RETURN_TO_LOBBY:
if (socket === this.groupHostSocket) {
this.statusBecomeOver();
const data = JSON.stringify([GameEv.RETURN_TO_LOBBY]);
this.sockets.forEach((s) => { if (s !== socket && s.readyState === s.OPEN) { s.send(data); }});
this._terminate();
} else {
const data = JSON.stringify([GameEv.RETURN_TO_LOBBY, SOCKET_ID(socket)]);
this.sockets.forEach((s) => { if (s !== socket && s.readyState === s.OPEN) s.send(data); });
}
break;
default: break;
}
}
/**
* Handles game-related events and attaches listeners to each client
* socket.
* @final
*/
export class ServerGame<S extends Coord.System = Coord.System> extends GameManager<S> {
readonly #deleteExternalRefs: () => void;
readonly #wsMessageCb: (ev: WebSocket.MessageEvent) => void;
protected readonly sockets: Set<WebSocket>;
protected readonly groupHostSocket: WebSocket;
public override get currentClientPlayer(): never {
throw new Error("never");
}
/** */
public constructor(args: Readonly<{
sockets: IterableIterator<WebSocket>,
groupHostSocket: WebSocket,
deleteExternalRefs: () => void,
gameDesc: Game.CtorArgs.UnFin<S>,
}>) {
super({
impl: {
gridClassLookup: GetGridImpl,
ClientPlayer: undefined,
RobotPlayer: GetRobotImpl,
onGameBecomeOver: () => {},
},
desc: (() => {
Player.CtorArgs.finalize(args.gameDesc); // <- mutation
return args.gameDesc;
})(),
clientPlayerIds: [],
});
this.sockets = new Set(args.sockets); // shallow copy
this.groupHostSocket = args.groupHostSocket;
this.#deleteExternalRefs = args.deleteExternalRefs;
JsUtils.instNoEnum (this as ServerGame<S>, "clientPlayers");
JsUtils.propNoWrite(this as ServerGame<S>, "sockets", "groupHostSocket");
this.#wsMessageCb = gameOnSocketMessage.bind(this as ServerGame<any>);
Object.seal(this); //🧊
this.sockets.forEach((s) => {
s.addEventListener("message", this.#wsMessageCb);
s.addEventListener("close", () => {
if (this.sockets.size === 1) {
this._terminate();
}
}, { once: true });
});
this.#greetGameSockets(args.gameDesc);
}
/** Helper for the constructor */
#greetGameSockets(gameDesc: Game.CtorArgs<S>): void {
// The below cast is safe because GamepartBase reassigns
// `gameDesc.playerDescs` the result of `Player.finalize`.
const humans = (
(gameDesc.players).filter((player) => player.familyId === "Human") as Player._CtorArgs["Human"][]
).freeze();
// Pass on Game constructor arguments to each client:
Promise.all(Array.from(this.sockets, (s) =>
new Promise<void>((resolve) => {
// TODO.impl timeouts to handle edge-cases of users leaving. Also in reset().
s.addEventListener("message", (ev) => {
if (JSON.parse(ev.data)[0] === GameEv.RESET) {
resolve();
}
}, { once: true });
})
).freeze()).then(() =>
this.reset() //👂 "reset time!"
);
this.sockets.forEach((s) => {
const clientPlayerIds = humans
.filter((desc) => desc.socket === s)
.map((desc) => desc.playerId).freeze();
const data = JSON.stringify([GroupEv.CREATE_GAME, gameDesc, clientPlayerIds]);
if (s.readyState === s.OPEN) { s.send(data); } //📢 "get ready for reset"
});
}
public override async reset(): Promise<Game.ResetSer> {
// Be ready for clients to indicate readiness to unpause.
Promise.all(Array.from(this.sockets, (s) =>
new Promise<void>((resolve) => {
s.addEventListener("message", (ev) => {
if (JSON.parse(ev.data)[0] === GameEv.UNPAUSE) { // <- clients send this upon receiving resetSer
resolve();
}
}, { once: true });
})
).freeze()).then(() => {
this.statusBecomePlaying(); //👂 "play time!"
});
const resetSer = await super.reset();
const data = JSON.stringify([GameEv.RESET, resetSer]);
this.sockets.forEach((s) => { if (s.readyState === s.OPEN) { s.send(data); }}); //📢 "get ready for playing!"
return resetSer;
}
public override setCurrentClientPlayer(index: number): void {
// no-op
}
public override setTimeout(callback: () => void, millis: number, ...args: any[]): number {
return setTimeout(callback, millis, args).unref() as unknown as number;
}
public override cancelTimeout(handle: number): void {
clearTimeout(handle as unknown as NodeJS.Timer);
}
public override statusBecomePlaying(): void {
super.statusBecomePlaying();
const data = JSON.stringify([GameEv.UNPAUSE]);
this.sockets.forEach((s) => { if (s.readyState === s.OPEN) { s.send(data); }});
}
public override statusBecomePaused(): void {
super.statusBecomePaused();
const data = JSON.stringify([GameEv.PAUSE]);
this.sockets.forEach((s) => { if (s.readyState === s.OPEN) { s.send(data); }});
}
public override commitStateChange(desc: StateChange.Res, authorSock?: WebSocket): void {
super.commitStateChange(desc, authorSock);
if (desc.rejectId) {
// The request was rejected- Notify the requester.
const data = JSON.stringify([GameEv.IN_GAME, desc]);
authorSock?.send(data);
} else {
const data = JSON.stringify([GameEv.IN_GAME, desc]);
this.sockets.forEach((s) => { if (s.readyState === s.OPEN) { s.send(data); }});
}
}
protected _terminate(): void {
this.sockets.forEach((s) => {
s.removeEventListener("message", this.#wsMessageCb);
});
this.#deleteExternalRefs();
}
}
JsUtils.protoNoEnum(ServerGame,
"setCurrentClientPlayer", "_terminate",
);
Object.freeze(ServerGame);
Object.freeze(ServerGame.prototype);