@phalanx-engine/server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +507 -0
  2. package/dist/Phalanx.d.ts +85 -0
  3. package/dist/Phalanx.d.ts.map +1 -0
  4. package/dist/Phalanx.js +600 -0
  5. package/dist/Phalanx.js.map +1 -0
  6. package/dist/config/defaults.d.ts +10 -0
  7. package/dist/config/defaults.d.ts.map +1 -0
  8. package/dist/config/defaults.js +51 -0
  9. package/dist/config/defaults.js.map +1 -0
  10. package/dist/config/validation.d.ts +15 -0
  11. package/dist/config/validation.d.ts.map +1 -0
  12. package/dist/config/validation.js +74 -0
  13. package/dist/config/validation.js.map +1 -0
  14. package/dist/index.d.ts +12 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +14 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/services/GameRoom.d.ts +359 -0
  19. package/dist/services/GameRoom.d.ts.map +1 -0
  20. package/dist/services/GameRoom.js +1272 -0
  21. package/dist/services/GameRoom.js.map +1 -0
  22. package/dist/services/MatchmakingService.d.ts +102 -0
  23. package/dist/services/MatchmakingService.d.ts.map +1 -0
  24. package/dist/services/MatchmakingService.js +286 -0
  25. package/dist/services/MatchmakingService.js.map +1 -0
  26. package/dist/services/OAuthExchangeService.d.ts +49 -0
  27. package/dist/services/OAuthExchangeService.d.ts.map +1 -0
  28. package/dist/services/OAuthExchangeService.js +96 -0
  29. package/dist/services/OAuthExchangeService.js.map +1 -0
  30. package/dist/services/PrivateRoomService.d.ts +136 -0
  31. package/dist/services/PrivateRoomService.d.ts.map +1 -0
  32. package/dist/services/PrivateRoomService.js +395 -0
  33. package/dist/services/PrivateRoomService.js.map +1 -0
  34. package/dist/services/TokenValidator.d.ts +60 -0
  35. package/dist/services/TokenValidator.d.ts.map +1 -0
  36. package/dist/services/TokenValidator.js +213 -0
  37. package/dist/services/TokenValidator.js.map +1 -0
  38. package/dist/types/index.d.ts +390 -0
  39. package/dist/types/index.d.ts.map +1 -0
  40. package/dist/types/index.js +6 -0
  41. package/dist/types/index.js.map +1 -0
  42. package/dist/utils/DeterministicRandom.d.ts +88 -0
  43. package/dist/utils/DeterministicRandom.d.ts.map +1 -0
  44. package/dist/utils/DeterministicRandom.js +140 -0
  45. package/dist/utils/DeterministicRandom.js.map +1 -0
  46. package/dist/utils/FixedMath.d.ts +22 -0
  47. package/dist/utils/FixedMath.d.ts.map +1 -0
  48. package/dist/utils/FixedMath.js +26 -0
  49. package/dist/utils/FixedMath.js.map +1 -0
  50. package/dist/utils/index.d.ts +7 -0
  51. package/dist/utils/index.d.ts.map +1 -0
  52. package/dist/utils/index.js +6 -0
  53. package/dist/utils/index.js.map +1 -0
  54. package/package.json +62 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Game mode presets configuration
3
+ */
4
+ export const GAME_MODES = {
5
+ '1v1': { playersPerMatch: 2, teamsCount: 2 },
6
+ '2v2': { playersPerMatch: 4, teamsCount: 2 },
7
+ '3v3': { playersPerMatch: 6, teamsCount: 2 },
8
+ '4v4': { playersPerMatch: 8, teamsCount: 2 },
9
+ FFA4: { playersPerMatch: 4, teamsCount: 4 },
10
+ };
11
+ /**
12
+ * Default configuration values
13
+ */
14
+ export const DEFAULT_CONFIG = {
15
+ // Server
16
+ port: 3000,
17
+ cors: { origin: '*' },
18
+ // Tick System
19
+ tickRate: 20,
20
+ tickDeadlineMs: 50,
21
+ tickMode: 'continuous',
22
+ // Only applies to tickMode: 'event' — ignored in continuous mode
23
+ turnTimeoutMs: 60000,
24
+ gameTypes: [],
25
+ // Matchmaking
26
+ gameMode: '1v1',
27
+ matchmakingIntervalMs: 1000,
28
+ countdownSeconds: 5,
29
+ // Timeouts
30
+ timeoutTicks: 40,
31
+ disconnectTicks: 100,
32
+ reconnectGracePeriodMs: 30000,
33
+ // Command Validation
34
+ maxTickBehind: 10,
35
+ maxTickAhead: 5,
36
+ // Command History (for reconnection)
37
+ commandHistoryTicks: 200, // 10 seconds at 20 TPS
38
+ // Determinism Features (Phase 2.1) - disabled by default for backward compatibility
39
+ validateInputSequence: false,
40
+ enableStateHashing: false,
41
+ stateHashInterval: 60, // 3 seconds at 20 TPS
42
+ // Ready Handshake
43
+ readyTimeoutMs: 30000,
44
+ // Desync Detection Behavior
45
+ desync: {
46
+ enabled: true,
47
+ action: 'end-match', // 'log-only' | 'end-match'
48
+ gracePeriodTicks: 1, // Consecutive desyncs before action
49
+ },
50
+ };
51
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,CAAC,MAAM,UAAU,GAA2C;IAChE,KAAK,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;IAC5C,KAAK,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;IAC5C,KAAK,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;IAC5C,KAAK,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;IAC5C,IAAI,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;CAC5C,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAkB;IAC3C,SAAS;IACT,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE;IAErB,cAAc;IACd,QAAQ,EAAE,EAAE;IACZ,cAAc,EAAE,EAAE;IAClB,QAAQ,EAAE,YAAY;IACtB,iEAAiE;IACjE,aAAa,EAAE,KAAK;IACpB,SAAS,EAAE,EAAE;IAEb,cAAc;IACd,QAAQ,EAAE,KAAK;IACf,qBAAqB,EAAE,IAAI;IAC3B,gBAAgB,EAAE,CAAC;IAEnB,WAAW;IACX,YAAY,EAAE,EAAE;IAChB,eAAe,EAAE,GAAG;IACpB,sBAAsB,EAAE,KAAK;IAE7B,qBAAqB;IACrB,aAAa,EAAE,EAAE;IACjB,YAAY,EAAE,CAAC;IAEf,qCAAqC;IACrC,mBAAmB,EAAE,GAAG,EAAE,uBAAuB;IAEjD,oFAAoF;IACpF,qBAAqB,EAAE,KAAK;IAC5B,kBAAkB,EAAE,KAAK;IACzB,iBAAiB,EAAE,EAAE,EAAE,sBAAsB;IAE7C,kBAAkB;IAClB,cAAc,EAAE,KAAK;IAErB,4BAA4B;IAC5B,MAAM,EAAE;QACN,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,WAAW,EAAE,2BAA2B;QAChD,gBAAgB,EAAE,CAAC,EAAE,oCAAoC;KAC1D;CACF,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { PhalanxConfig } from '../types/index.js';
2
+ /**
3
+ * Validates and merges user configuration with defaults
4
+ * @param userConfig - Partial user configuration
5
+ * @returns Complete validated configuration
6
+ */
7
+ export declare function validateConfig(userConfig?: Partial<PhalanxConfig>): PhalanxConfig;
8
+ /**
9
+ * Gets the resolved game mode (players per match and teams count)
10
+ */
11
+ export declare function resolveGameMode(gameMode: PhalanxConfig['gameMode']): {
12
+ playersPerMatch: number;
13
+ teamsCount: number;
14
+ };
15
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/config/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,UAAU,GAAE,OAAO,CAAC,aAAa,CAAM,GACtC,aAAa,CAuEf;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG;IACpE,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB,CAKA"}
@@ -0,0 +1,74 @@
1
+ import { DEFAULT_CONFIG, GAME_MODES } from './defaults.js';
2
+ /**
3
+ * Validates and merges user configuration with defaults
4
+ * @param userConfig - Partial user configuration
5
+ * @returns Complete validated configuration
6
+ */
7
+ export function validateConfig(userConfig = {}) {
8
+ const config = {
9
+ ...DEFAULT_CONFIG,
10
+ ...userConfig,
11
+ cors: {
12
+ ...DEFAULT_CONFIG.cors,
13
+ ...(userConfig.cors || {}),
14
+ },
15
+ desync: {
16
+ ...DEFAULT_CONFIG.desync,
17
+ ...(userConfig.desync || {}),
18
+ },
19
+ };
20
+ // Validate port
21
+ if (config.port < 1 || config.port > 65535) {
22
+ throw new Error(`Invalid port: ${config.port}. Must be between 1 and 65535.`);
23
+ }
24
+ // Validate tickRate
25
+ if (config.tickRate < 1 || config.tickRate > 128) {
26
+ throw new Error(`Invalid tickRate: ${config.tickRate}. Must be between 1 and 128.`);
27
+ }
28
+ // Validate tickDeadlineMs
29
+ if (config.tickDeadlineMs < 1) {
30
+ throw new Error(`Invalid tickDeadlineMs: ${config.tickDeadlineMs}. Must be positive.`);
31
+ }
32
+ // Validate gameMode
33
+ if (typeof config.gameMode === 'string') {
34
+ if (!GAME_MODES[config.gameMode]) {
35
+ throw new Error(`Invalid gameMode: ${config.gameMode}. Valid presets: ${Object.keys(GAME_MODES).join(', ')}`);
36
+ }
37
+ }
38
+ else if (typeof config.gameMode === 'object') {
39
+ if (config.gameMode.playersPerMatch < 2) {
40
+ throw new Error('playersPerMatch must be at least 2');
41
+ }
42
+ if (config.gameMode.teamsCount < 1) {
43
+ throw new Error('teamsCount must be at least 1');
44
+ }
45
+ if (config.gameMode.teamsCount > config.gameMode.playersPerMatch) {
46
+ throw new Error('teamsCount cannot exceed playersPerMatch');
47
+ }
48
+ }
49
+ // Validate timeout values
50
+ if (config.timeoutTicks < 1) {
51
+ throw new Error('timeoutTicks must be positive');
52
+ }
53
+ if (config.disconnectTicks < config.timeoutTicks) {
54
+ throw new Error('disconnectTicks must be >= timeoutTicks');
55
+ }
56
+ // Validate tick range limits
57
+ if (config.maxTickBehind < 1) {
58
+ throw new Error('maxTickBehind must be positive');
59
+ }
60
+ if (config.maxTickAhead < 1) {
61
+ throw new Error('maxTickAhead must be positive');
62
+ }
63
+ return config;
64
+ }
65
+ /**
66
+ * Gets the resolved game mode (players per match and teams count)
67
+ */
68
+ export function resolveGameMode(gameMode) {
69
+ if (typeof gameMode === 'string') {
70
+ return GAME_MODES[gameMode];
71
+ }
72
+ return gameMode;
73
+ }
74
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/config/validation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3D;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,aAAqC,EAAE;IAEvC,MAAM,MAAM,GAAkB;QAC5B,GAAG,cAAc;QACjB,GAAG,UAAU;QACb,IAAI,EAAE;YACJ,GAAG,cAAc,CAAC,IAAI;YACtB,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,EAAE,CAAC;SAC3B;QACD,MAAM,EAAE;YACN,GAAG,cAAc,CAAC,MAAM;YACxB,GAAG,CAAC,UAAU,CAAC,MAAM,IAAI,EAAE,CAAC;SAC7B;KACF,CAAC;IAEF,gBAAgB;IAChB,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CACb,iBAAiB,MAAM,CAAC,IAAI,gCAAgC,CAC7D,CAAC;IACJ,CAAC;IAED,oBAAoB;IACpB,IAAI,MAAM,CAAC,QAAQ,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,GAAG,GAAG,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CACb,qBAAqB,MAAM,CAAC,QAAQ,8BAA8B,CACnE,CAAC;IACJ,CAAC;IAED,0BAA0B;IAC1B,IAAI,MAAM,CAAC,cAAc,GAAG,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,2BAA2B,MAAM,CAAC,cAAc,qBAAqB,CACtE,CAAC;IACJ,CAAC;IAED,oBAAoB;IACpB,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACxC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CACb,qBAAqB,MAAM,CAAC,QAAQ,oBAAoB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC7F,CAAC;QACJ,CAAC;IACH,CAAC;SAAM,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC/C,IAAI,MAAM,CAAC,QAAQ,CAAC,eAAe,GAAG,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACxD,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACnD,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAC;YACjE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IAED,0BAA0B;IAC1B,IAAI,MAAM,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,MAAM,CAAC,eAAe,GAAG,MAAM,CAAC,YAAY,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IAED,6BAA6B;IAC7B,IAAI,MAAM,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,CAAC;IACD,IAAI,MAAM,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,QAAmC;IAIjE,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Phalanx Engine
3
+ * A game-agnostic deterministic lockstep multiplayer engine
4
+ */
5
+ export { Phalanx } from './Phalanx.js';
6
+ export { TokenValidatorService, createDevValidator, createEndpointValidator, } from './services/TokenValidator.js';
7
+ export { DeterministicRandom } from './utils/index.js';
8
+ export { FP, FPVector2, FPVector3, FixedPoint } from './utils/index.js';
9
+ export type { FPVector2Interface, FPVector3Interface } from './utils/index.js';
10
+ export type { PhalanxConfig, PlayerCommand, MatchFoundEvent, GameStartEvent, TickSyncEvent, CommandsBatchEvent, TickCommands, SubmitCommandsEvent, PlayerInfo, MatchInfo, GameMode, GameModePreset, CustomGameMode, CorsConfig, TlsConfig, QueuedPlayer, QueueStatusEvent, StateHashEvent, DesyncDetectedEvent, PhalanxEventType, PhalanxEventHandlers, AuthConfig, TokenValidator, TokenValidationResult, DesyncConfig, DesyncAction, PauseConfig, TickMode, GameTypeConfig, RoomCreatedEvent, RoomRecoveredEvent, RoomErrorEvent, } from './types/index.js';
11
+ export { GAME_MODES } from './config/defaults.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACxE,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG/E,YAAY,EACV,aAAa,EACb,aAAa,EACb,eAAe,EACf,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,mBAAmB,EACnB,UAAU,EACV,SAAS,EACT,QAAQ,EACR,cAAc,EACd,cAAc,EACd,UAAU,EACV,SAAS,EACT,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EAEpB,UAAU,EACV,cAAc,EACd,qBAAqB,EAErB,YAAY,EACZ,YAAY,EAEZ,WAAW,EAEX,QAAQ,EACR,cAAc,EAEd,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Phalanx Engine
3
+ * A game-agnostic deterministic lockstep multiplayer engine
4
+ */
5
+ // Main class
6
+ export { Phalanx } from './Phalanx.js';
7
+ // Authentication
8
+ export { TokenValidatorService, createDevValidator, createEndpointValidator, } from './services/TokenValidator.js';
9
+ // Utilities
10
+ export { DeterministicRandom } from './utils/index.js';
11
+ export { FP, FPVector2, FPVector3, FixedPoint } from './utils/index.js';
12
+ // Constants
13
+ export { GAME_MODES } from './config/defaults.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,aAAa;AACb,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,iBAAiB;AACjB,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,8BAA8B,CAAC;AAEtC,YAAY;AACZ,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AA4CxE,YAAY;AACZ,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,359 @@
1
+ import type { Server as SocketIOServer } from 'socket.io';
2
+ import type { PhalanxConfig, QueuedPlayer, MatchInfo, PlayerCommand, TickCommands } from '../types/index.js';
3
+ /**
4
+ * Game Room
5
+ * Handles a single match with tick synchronization and command broadcasting
6
+ */
7
+ export declare class GameRoom {
8
+ private readonly id;
9
+ private readonly roomId;
10
+ private readonly io;
11
+ private readonly config;
12
+ private readonly players;
13
+ private readonly socketToPlayer;
14
+ private readonly teams;
15
+ private readonly eventEmitter;
16
+ private currentTick;
17
+ private state;
18
+ private createdAt;
19
+ private tickInterval;
20
+ private countdownTimer;
21
+ private countdownInterval;
22
+ /**
23
+ * Set when the match enters `'waiting-for-players'` (i.e. start()
24
+ * was called but at least one participant's socket was offline).
25
+ * Fires after `playersConnectTimeoutMs` and ends the match with
26
+ * `match-end: 'players-not-connected'` so the connected players
27
+ * aren't stranded indefinitely waiting for someone who's never
28
+ * coming back.
29
+ *
30
+ * Cleared in `transitionFromWaitingForPlayers` when everyone's
31
+ * online and we proceed to the countdown, or in `stop()` on shutdown.
32
+ */
33
+ private playersConnectTimeout;
34
+ /**
35
+ * How long we'll hold the deferred-start state before giving up on
36
+ * absent participants. Long enough to cover a typical mobile
37
+ * "switched to messenger" round-trip, short enough that the
38
+ * connected player isn't staring at a frozen lobby. Pulled from
39
+ * config when available so games can tune it.
40
+ */
41
+ private readonly playersConnectTimeoutMs;
42
+ /**
43
+ * Absolute epoch-ms deadline for the countdown, set when the countdown
44
+ * starts and cleared when `game-start` is emitted. Used to compute the
45
+ * remaining seconds for a late-joining socket (e.g. a private-room host
46
+ * whose mobile browser killed the WebSocket while they were sharing the
47
+ * invite link) so the client can render the correct number instead of
48
+ * staying stuck on the last value it saw before disconnecting.
49
+ */
50
+ private countdownDeadline;
51
+ /**
52
+ * Set to true once `game-start` has been broadcast. A host who recovers
53
+ * after this point needs to synthesize the event locally — we signal
54
+ * that via this flag in the `reconnect-state` payload so the client can
55
+ * transition out of the countdown UI without waiting for a second
56
+ * `game-start` it will never receive.
57
+ */
58
+ private gameStartEmitted;
59
+ private pendingCommands;
60
+ private readyPlayers;
61
+ private readyTimeout;
62
+ private readonly readyTimeoutMs;
63
+ private commandBuffer;
64
+ private tickSubmissions;
65
+ private lastMessageTime;
66
+ private commandHistory;
67
+ private laggingPlayers;
68
+ private readonly randomSeed;
69
+ private lastSequence;
70
+ private stateHashes;
71
+ private consecutiveDesyncs;
72
+ private readonly desyncConfig;
73
+ private readonly pauseConfig;
74
+ private pauseCount;
75
+ private pausedByPlayerId;
76
+ private readonly gameType;
77
+ private readonly tickMode;
78
+ private turnTimeout;
79
+ constructor(id: string, io: SocketIOServer, config: PhalanxConfig, teams: QueuedPlayer[][], eventEmitter: (event: string, ...args: unknown[]) => boolean | void, gameType?: string);
80
+ /**
81
+ * Start the game room.
82
+ *
83
+ * If every participant's socket is currently live we proceed straight
84
+ * to the countdown. Otherwise the room enters
85
+ * `'waiting-for-players'` — `match-found` and
86
+ * `match-waiting-for-players` are emitted to whoever is online, and
87
+ * the countdown is held back until either:
88
+ *
89
+ * - every player has reconnected (via `GameRoom.handleReconnect`,
90
+ * typically driven by the engine's room/match-recover handlers
91
+ * for the private-room case), at which point we transition to
92
+ * the countdown; OR
93
+ *
94
+ * - `playersConnectTimeoutMs` elapses and we end the match with
95
+ * `match-end: 'players-not-connected'`, freeing the participants
96
+ * who DID show up.
97
+ *
98
+ * Putting this gate inside `GameRoom` (rather than at the
99
+ * matchmaking / private-room layer) means every flow that builds a
100
+ * match — public matchmaking, private invites, future custom
101
+ * lobbies — gets the same "don't drop a single player into a
102
+ * countdown alone" guarantee for free.
103
+ */
104
+ start(): void;
105
+ /**
106
+ * Drive the deferred-start state machine forward: invoked from
107
+ * `handleReconnect` once a previously-missing player has rebound
108
+ * their socket. If everyone is now present, cancels the players-
109
+ * connect timeout and transitions into the countdown; otherwise
110
+ * just re-broadcasts the updated waiting-for-players list.
111
+ */
112
+ private maybeBeginCountdownAfterReconnect;
113
+ /** True iff every participant's `connected` flag is set. */
114
+ private areAllPlayersConnected;
115
+ /** PlayerIds whose `connected` flag is currently false. */
116
+ private getDisconnectedPlayerIds;
117
+ /**
118
+ * Broadcast `match-waiting-for-players` to every currently-connected
119
+ * socket in the room, listing which playerIds we're still waiting on.
120
+ * Uses `this.io.to(this.roomId).emit(...)` so socket.io handles room
121
+ * routing to currently connected sockets while we include the canonical
122
+ * `matchId` in the payload, consistent with `notifyMatchFound`.
123
+ */
124
+ private notifyWaitingForPlayers;
125
+ /**
126
+ * Emit a lifecycle event directly to every currently-connected player.
127
+ *
128
+ * `match-found` is already sent as a direct socket event, while the
129
+ * countdown used to depend on Socket.IO room membership established a
130
+ * few lines earlier in `start()`. Mobile transports can race around
131
+ * polling/websocket upgrade or a brief reconnect: the host still sees
132
+ * `match-found` and switches to the countdown screen, but misses the
133
+ * room broadcast that should advance it. Direct delivery keeps the
134
+ * pre-game lifecycle level with the personalized match-found path.
135
+ */
136
+ private emitLifecycleToConnectedPlayers;
137
+ /**
138
+ * Start the game countdown
139
+ * Emits countdown events (5, 4, 3, 2, 1, 0) every second, then game-start
140
+ */
141
+ private startGameCountdown;
142
+ /**
143
+ * Transition to waiting-for-ready state.
144
+ * The tick loop will not begin until all clients send 'client-ready'.
145
+ */
146
+ private enterWaitingForReady;
147
+ private armReadyTimeout;
148
+ private clearReadyTimeout;
149
+ private armPlayersReconnectTimeout;
150
+ private clearPlayersReconnectTimeout;
151
+ /**
152
+ * Notify all players that a match has been found
153
+ * Each player receives personalized data about their teammates and opponents
154
+ */
155
+ private notifyMatchFound;
156
+ /**
157
+ * Build the personalized `match-found` payload for `playerId`.
158
+ *
159
+ * Used by the initial broadcast in {@link notifyMatchFound} and by
160
+ * retroactive delivery when a host who was offline at `joinRoom`
161
+ * time finally reconnects via `room-recover`. Returns `null` if
162
+ * the player is not in this match.
163
+ */
164
+ buildMatchFoundPayload(playerId: string): {
165
+ matchId: string;
166
+ playerId: string;
167
+ teamId: number;
168
+ teammates: {
169
+ playerId: string;
170
+ username: string;
171
+ }[];
172
+ opponents: {
173
+ playerId: string;
174
+ username: string;
175
+ }[];
176
+ } | null;
177
+ private buildMatchFoundPayloadForTeam;
178
+ /**
179
+ * Handle a player reporting ready after asset loading.
180
+ *
181
+ * The match only starts once *every* player in the room is both
182
+ * connected and has explicitly reported `client-ready`
183
+ * (see `areAllPlayersConnectedAndReady`). If any player is
184
+ * disconnected (e.g. mid-reconnect during `waiting-for-ready`),
185
+ * the gate stays closed even if all currently-connected players
186
+ * are ready.
187
+ *
188
+ * Once the gate opens:
189
+ * - In `continuous` tick mode the tick loop is started.
190
+ * - In `event` tick mode no tick loop is created; the turn
191
+ * timeout is armed via `resetTurnTimeout()` instead.
192
+ *
193
+ * @param playerId - The player reporting ready
194
+ */
195
+ handlePlayerReady(playerId: string): void;
196
+ /**
197
+ * End the match because not all clients reported ready within the timeout.
198
+ */
199
+ private endMatchDueToReadyTimeout;
200
+ /**
201
+ * Start the actual game (after countdown)
202
+ */
203
+ private startGame;
204
+ /**
205
+ * Process a single tick
206
+ */
207
+ private processTick;
208
+ /**
209
+ * Handle a player command
210
+ */
211
+ handleCommand(playerId: string, command: PlayerCommand): boolean;
212
+ /**
213
+ * Validate command sequence number (2.1.4)
214
+ * Returns true if sequence is valid, false otherwise
215
+ */
216
+ private validateCommandSequence;
217
+ /**
218
+ * Receive commands from a player for a specific tick (LOCKSTEP-2)
219
+ * Commands can be empty if player has no actions for this tick.
220
+ * This is normal - units may be moving/idle and player doesn't need to input anything.
221
+ */
222
+ receivePlayerCommands(playerId: string, tick: number, commands: PlayerCommand[]): {
223
+ accepted: boolean;
224
+ invalidCommands?: PlayerCommand[];
225
+ };
226
+ /**
227
+ * Get all commands for a specific tick
228
+ */
229
+ getCommandsForTick(tick: number): TickCommands | null;
230
+ /**
231
+ * Check if all players have submitted for a specific tick
232
+ */
233
+ allPlayersSubmittedForTick(tick: number): boolean;
234
+ /**
235
+ * Get which players have submitted for a specific tick
236
+ */
237
+ getSubmissionsForTick(tick: number): Set<string>;
238
+ /**
239
+ * Clean up old ticks after they've been processed
240
+ */
241
+ clearOldTicks(beforeTick: number): void;
242
+ /**
243
+ * Update player activity timestamp (called on any message from player)
244
+ * Uses real time instead of ticks - more reliable with Socket.IO ping/pong
245
+ */
246
+ updatePlayerActivity(playerId: string): void;
247
+ /**
248
+ * Check for lagging/disconnected players (LOCKSTEP-5)
249
+ * Uses real time (ms) instead of ticks for more reliable detection.
250
+ * Skipped entirely in event mode (turn timeout handles inactivity).
251
+ */
252
+ private checkPlayerTimeouts;
253
+ /**
254
+ * Reset (or start) the turn timeout for event mode.
255
+ * If no commands arrive within turnTimeoutMs the match ends.
256
+ */
257
+ private resetTurnTimeout;
258
+ /**
259
+ * End the match because the turn timeout expired (event mode only).
260
+ */
261
+ private endMatchDueToTurnTimeout;
262
+ /**
263
+ * Store command history for reconnection support (NET-2)
264
+ */
265
+ private storeCommandHistory;
266
+ /**
267
+ * Get recent command history for reconnecting player (NET-2)
268
+ */
269
+ getRecentCommandHistory(fromTick: number): {
270
+ tick: number;
271
+ commands: PlayerCommand[];
272
+ }[];
273
+ /**
274
+ * Pause the game.
275
+ * Stops the tick loop and broadcasts 'game-paused' to all clients.
276
+ * All clients freeze deterministically because the pause only takes effect
277
+ * when they receive this broadcast (after the last completed tick).
278
+ *
279
+ * @param requestedBy - Player ID who requested the pause
280
+ * @returns true if the game was paused, false if not in a pauseable state or player exceeded pause limit
281
+ */
282
+ pause(requestedBy: string): boolean;
283
+ /**
284
+ * Resume the game after a pause.
285
+ * Restarts the tick loop and broadcasts 'game-resumed' to all clients.
286
+ *
287
+ * @param requestedBy - Player ID who requested the resume
288
+ * @returns true if the game was resumed, false if not currently paused or player not allowed to resume
289
+ */
290
+ resume(requestedBy: string): boolean;
291
+ /**
292
+ * Get current tick number
293
+ */
294
+ getCurrentTick(): number;
295
+ /**
296
+ * Stop the game room
297
+ * @param skipNotify - If true, skip emitting match-ended events (used when already handled)
298
+ */
299
+ stop(skipNotify?: boolean): void;
300
+ /**
301
+ * Handle player disconnection (NET-2)
302
+ */
303
+ handleDisconnect(socketId: string): void;
304
+ /**
305
+ * Handle player reconnection (NET-2)
306
+ */
307
+ handleReconnect(playerId: string, socketId: string): boolean;
308
+ /**
309
+ * Returns true only when every player in the room is currently
310
+ * connected *and* has reported `client-ready`. This is the
311
+ * authoritative gate for leaving the `waiting-for-ready` state
312
+ * and starting the match (continuous or event tick mode).
313
+ *
314
+ * A disconnected player keeps the gate closed even if all other
315
+ * players are ready, which is required for correct reconnect
316
+ * behavior during `waiting-for-ready`.
317
+ */
318
+ private areAllPlayersConnectedAndReady;
319
+ /**
320
+ * Whether the given playerId is a participant of this match.
321
+ * Used by `PrivateRoomService.recoverRoom` to authenticate a
322
+ * `room-recover` against a deferred / running match without
323
+ * exposing the players map.
324
+ */
325
+ hasPlayer(playerId: string): boolean;
326
+ /**
327
+ * Get match information
328
+ */
329
+ getMatchInfo(): MatchInfo;
330
+ /**
331
+ * Get the room ID
332
+ */
333
+ getId(): string;
334
+ /**
335
+ * Get the random seed for this match
336
+ * Clients use this to initialize their deterministic RNG
337
+ */
338
+ getRandomSeed(): number;
339
+ /**
340
+ * Receive state hash from a player for a specific tick
341
+ * @param playerId - The player sending the hash
342
+ * @param tick - The tick this hash is for
343
+ * @param hash - The state hash string
344
+ */
345
+ receiveStateHash(playerId: string, tick: number, hash: string): void;
346
+ /**
347
+ * Check if there's a desync at a given tick
348
+ */
349
+ private checkForDesync;
350
+ /**
351
+ * End the match due to confirmed desync
352
+ */
353
+ private endMatchDueToDesync;
354
+ /**
355
+ * Clean up state hashes older than the specified tick
356
+ */
357
+ private cleanupOldStateHashes;
358
+ }
359
+ //# sourceMappingURL=GameRoom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GameRoom.d.ts","sourceRoot":"","sources":["../../src/services/GameRoom.ts"],"names":[],"mappings":"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"}