@gamerstake/game-core 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 (48) hide show
  1. package/.eslintrc.json +22 -0
  2. package/.testing-guide-summary.md +261 -0
  3. package/DEVELOPER_GUIDE.md +996 -0
  4. package/MANUAL_TESTING.md +369 -0
  5. package/QUICK_START.md +368 -0
  6. package/README.md +379 -0
  7. package/TESTING_OVERVIEW.md +378 -0
  8. package/dist/index.d.ts +1266 -0
  9. package/dist/index.js +1632 -0
  10. package/dist/index.js.map +1 -0
  11. package/examples/simple-game/README.md +176 -0
  12. package/examples/simple-game/client.ts +201 -0
  13. package/examples/simple-game/package.json +14 -0
  14. package/examples/simple-game/server.ts +233 -0
  15. package/jest.config.ts +39 -0
  16. package/package.json +54 -0
  17. package/src/core/GameLoop.ts +214 -0
  18. package/src/core/GameRules.ts +103 -0
  19. package/src/core/GameServer.ts +200 -0
  20. package/src/core/Room.ts +368 -0
  21. package/src/entities/Entity.ts +118 -0
  22. package/src/entities/Registry.ts +161 -0
  23. package/src/index.ts +51 -0
  24. package/src/input/Command.ts +41 -0
  25. package/src/input/InputQueue.ts +130 -0
  26. package/src/network/Network.ts +112 -0
  27. package/src/network/Snapshot.ts +59 -0
  28. package/src/physics/AABB.ts +104 -0
  29. package/src/physics/Movement.ts +124 -0
  30. package/src/spatial/Grid.ts +202 -0
  31. package/src/types/index.ts +117 -0
  32. package/src/types/protocol.ts +161 -0
  33. package/src/utils/Logger.ts +112 -0
  34. package/src/utils/RingBuffer.ts +116 -0
  35. package/tests/AABB.test.ts +38 -0
  36. package/tests/Entity.test.ts +35 -0
  37. package/tests/GameLoop.test.ts +58 -0
  38. package/tests/GameServer.test.ts +64 -0
  39. package/tests/Grid.test.ts +28 -0
  40. package/tests/InputQueue.test.ts +42 -0
  41. package/tests/Movement.test.ts +37 -0
  42. package/tests/Network.test.ts +39 -0
  43. package/tests/Registry.test.ts +36 -0
  44. package/tests/RingBuffer.test.ts +38 -0
  45. package/tests/Room.test.ts +80 -0
  46. package/tests/Snapshot.test.ts +19 -0
  47. package/tsconfig.json +28 -0
  48. package/tsup.config.ts +14 -0
@@ -0,0 +1,996 @@
1
+ # Developer Guide - @gamerstake/game-core
2
+
3
+ > **A comprehensive guide to building multiplayer games with game-core**
4
+
5
+ This guide will teach you everything you need to know to build multiplayer games using the `@gamerstake/game-core` engine.
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Introduction](#introduction)
10
+ 2. [Core Concepts](#core-concepts)
11
+ 3. [Getting Started](#getting-started)
12
+ 4. [Architecture Overview](#architecture-overview)
13
+ 5. [Building Your First Game](#building-your-first-game)
14
+ 6. [Advanced Topics](#advanced-topics)
15
+ 7. [Performance Optimization](#performance-optimization)
16
+ 8. [Common Patterns](#common-patterns)
17
+ 9. [Troubleshooting](#troubleshooting)
18
+ 10. [Best Practices](#best-practices)
19
+
20
+ ---
21
+
22
+ ## Introduction
23
+
24
+ `@gamerstake/game-core` is a **game-agnostic multiplayer engine** that handles the complex infrastructure of real-time multiplayer games so you can focus on your game logic.
25
+
26
+ ### What game-core provides
27
+
28
+ **Fixed tick rate game loop** (20 TPS with drift compensation)
29
+ **Authoritative server architecture** (server is the source of truth)
30
+ **Client-server networking** (via Socket.io)
31
+ **State synchronization** (full snapshots + delta updates)
32
+ **Input buffering** (reliable command processing)
33
+ **Entity management** (ECS-lite with dirty tracking)
34
+ **Spatial partitioning** (grid-based O(1) queries)
35
+ **Basic 2D physics** (velocity, position, AABB collision)
36
+ **Performance monitoring** (real-time metrics)
37
+
38
+ ### What you need to provide
39
+
40
+ **Game rules** (implement the `GameRules` interface)
41
+ **Game client** (render game state, send inputs)
42
+ **Persistence** (optional - save/load game state)
43
+ **Authentication** (optional - player identity)
44
+
45
+ ---
46
+
47
+ ## Core Concepts
48
+
49
+ ### 1. Game Server
50
+
51
+ The **GameServer** manages multiple rooms (game instances).
52
+
53
+ ```typescript
54
+ const gameServer = new GameServer();
55
+ gameServer.setServer(io); // Socket.io instance
56
+ ```
57
+
58
+ - One GameServer per process
59
+ - Can host multiple rooms
60
+ - Handles room lifecycle
61
+
62
+ ### 2. Room
63
+
64
+ A **Room** is a single game instance (match, level, world).
65
+
66
+ ```typescript
67
+ const room = gameServer.createRoom('room-1', gameRules, config);
68
+ ```
69
+
70
+ - Has its own game loop running at fixed tick rate (default: 20 TPS)
71
+ - Contains entities (players, items, NPCs)
72
+ - Manages networking, spatial partitioning, input processing
73
+ - Can be **persistent** (metaverse) or **match-based** (battle royale)
74
+
75
+ ### 3. GameRules Interface
76
+
77
+ The **GameRules** interface is where YOU implement your game logic.
78
+
79
+ ```typescript
80
+ interface GameRules {
81
+ onRoomCreated(room: Room): void;
82
+ onPlayerJoin(room: Room, player: Entity): void;
83
+ onPlayerLeave(room: Room, playerId: string): void;
84
+ onTick(room: Room, delta: number): void;
85
+ onCommand(room: Room, playerId: string, command: Command): void;
86
+ shouldEndRoom(room: Room): boolean;
87
+ }
88
+ ```
89
+
90
+ **Think of it as hooks** - the engine calls your code at the right times.
91
+
92
+ ### 4. Entity
93
+
94
+ An **Entity** represents any game object (player, NPC, item, projectile).
95
+
96
+ ```typescript
97
+ const player = new Entity('player-1', x, y);
98
+ player.setVelocity(100, 0); // Move right at 100 units/sec
99
+ player.updatePosition(deltaMs); // Apply velocity
100
+ ```
101
+
102
+ **Key features:**
103
+
104
+ - Position (x, y) and velocity (vx, vy)
105
+ - Dirty flag tracking (only broadcast changed entities)
106
+ - Extensible (subclass for custom properties)
107
+
108
+ ### 5. Registry
109
+
110
+ The **Registry** manages entity lifecycle (add, remove, iterate).
111
+
112
+ ```typescript
113
+ room.getRegistry().forEach((entity) => {
114
+ // Process all entities
115
+ });
116
+
117
+ const player = room.getRegistry().get(playerId);
118
+ ```
119
+
120
+ ### 6. Grid (Spatial Partitioning)
121
+
122
+ The **Grid** divides the world into cells for efficient spatial queries.
123
+
124
+ ```typescript
125
+ const nearby = room.getGrid().getNearbyEntities(x, y, range);
126
+ ```
127
+
128
+ - **O(1) queries** instead of O(n²) distance checks
129
+ - Essential for collision detection, visibility, etc.
130
+
131
+ ### 7. Networking
132
+
133
+ The **Network** layer abstracts Socket.io and handles state broadcasting.
134
+
135
+ ```typescript
136
+ room.broadcast({ op: 'GAME_EVENT', data: ... });
137
+ room.sendTo(playerId, { op: 'PRIVATE_MSG', msg: 'Hello' });
138
+ ```
139
+
140
+ ### 8. Snapshot System
141
+
142
+ **Snapshots** synchronize game state to clients.
143
+
144
+ - **Full snapshot** on initial connection (all entities)
145
+ - **Delta updates** every tick (only changed entities)
146
+ - Bandwidth efficient
147
+
148
+ ---
149
+
150
+ ## Getting Started
151
+
152
+ ### Prerequisites
153
+
154
+ - Node.js 20+
155
+ - TypeScript 5.3+
156
+ - Socket.io 4.7+
157
+
158
+ ### Installation
159
+
160
+ ```bash
161
+ pnpm add @gamerstake/game-core socket.io
162
+ pnpm add -D @types/node typescript
163
+ ```
164
+
165
+ ### Project Structure
166
+
167
+ ```
168
+ my-game/
169
+ server/
170
+ index.ts # Server entry point
171
+ GameRules.ts # Your game logic
172
+ entities/
173
+ Player.ts # Custom entity types
174
+ client/
175
+ index.ts # Client entry point
176
+ renderer.ts # Game rendering
177
+ shared/
178
+ types.ts # Shared types
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Architecture Overview
184
+
185
+ ### Data Flow
186
+
187
+ ```
188
+ Client Server
189
+
190
+ C_MOVE (input) >
191
+
192
+ [InputQueue]
193
+
194
+ [Game Tick]
195
+ - Process inputs
196
+ - Update entities
197
+ - Check collisions
198
+
199
+ < S_UPDATE (state)
200
+
201
+ [Render]
202
+ ```
203
+
204
+ ### Tick Cycle (20 TPS = 50ms per tick)
205
+
206
+ ```
207
+ 1. Process queued inputs (onCommand)
208
+ 2. Update game state (onTick)
209
+ 3. Check win conditions (shouldEndRoom)
210
+ 4. Broadcast state changes (automatic)
211
+ 5. Sleep until next tick
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Building Your First Game
217
+
218
+ Let's build a simple **multiplayer tag game** where players chase each other!
219
+
220
+ ### Step 1: Define Game Rules
221
+
222
+ ```typescript
223
+ // server/TagGameRules.ts
224
+ import { GameRules, Room, Entity, Command, MoveCommand } from '@gamerstake/game-core';
225
+
226
+ interface TagPlayer extends Entity {
227
+ isIt: boolean;
228
+ }
229
+
230
+ export class TagGameRules implements GameRules<TagPlayer> {
231
+ onRoomCreated(room: Room<TagPlayer>): void {
232
+ console.log('Tag game created! Waiting for players...');
233
+ }
234
+
235
+ onPlayerJoin(room: Room<TagPlayer>, player: TagPlayer): void {
236
+ // Spawn at random position
237
+ player.setPosition(Math.random() * 1000, Math.random() * 1000);
238
+
239
+ // First player is "it"
240
+ const players = Array.from(room.getRegistry().values());
241
+ player.isIt = players.length === 1;
242
+
243
+ room.broadcast({
244
+ op: 'PLAYER_JOINED',
245
+ playerId: player.id,
246
+ isIt: player.isIt,
247
+ });
248
+ }
249
+
250
+ onPlayerLeave(room: Room<TagPlayer>, playerId: string): void {
251
+ room.broadcast({
252
+ op: 'PLAYER_LEFT',
253
+ playerId,
254
+ });
255
+
256
+ // If "it" player left, pick a new one
257
+ const players = Array.from(room.getRegistry().values());
258
+ if (players.length > 0 && !players.some((p) => p.isIt)) {
259
+ players[0].isIt = true;
260
+ room.broadcast({
261
+ op: 'NEW_IT',
262
+ playerId: players[0].id,
263
+ });
264
+ }
265
+ }
266
+
267
+ onTick(room: Room<TagPlayer>, delta: number): void {
268
+ const players = Array.from(room.getRegistry().values());
269
+
270
+ // Update positions
271
+ players.forEach((p) => {
272
+ if (p.vx !== 0 || p.vy !== 0) {
273
+ p.updatePosition(delta);
274
+
275
+ // Keep in bounds
276
+ p.x = Math.max(0, Math.min(1000, p.x));
277
+ p.y = Math.max(0, Math.min(1000, p.y));
278
+ }
279
+ });
280
+
281
+ // Check for tag collisions
282
+ const itPlayer = players.find((p) => p.isIt);
283
+ if (!itPlayer) return;
284
+
285
+ for (const other of players) {
286
+ if (other.id === itPlayer.id) continue;
287
+
288
+ const dist = Math.hypot(itPlayer.x - other.x, itPlayer.y - other.y);
289
+
290
+ if (dist < 50) {
291
+ // Tag range
292
+ // Tagged!
293
+ itPlayer.isIt = false;
294
+ other.isIt = true;
295
+
296
+ room.broadcast({
297
+ op: 'TAGGED',
298
+ tagger: itPlayer.id,
299
+ tagged: other.id,
300
+ });
301
+
302
+ break;
303
+ }
304
+ }
305
+ }
306
+
307
+ onCommand(room: Room<TagPlayer>, playerId: string, command: Command): void {
308
+ const player = room.getRegistry().get(playerId);
309
+ if (!player) return;
310
+
311
+ if (command.type === 'move') {
312
+ const moveCmd = command as MoveCommand;
313
+ const speed = player.isIt ? 250 : 200; // "It" is faster!
314
+
315
+ const len = Math.hypot(moveCmd.dir.x, moveCmd.dir.y);
316
+ if (len > 0) {
317
+ player.setVelocity((moveCmd.dir.x / len) * speed, (moveCmd.dir.y / len) * speed);
318
+ }
319
+ } else if (command.type === 'stop') {
320
+ player.setVelocity(0, 0);
321
+ }
322
+ }
323
+
324
+ shouldEndRoom(room: Room<TagPlayer>): boolean {
325
+ // End if no players left
326
+ return room.getRegistry().size === 0;
327
+ }
328
+ }
329
+ ```
330
+
331
+ ### Step 2: Create the Server
332
+
333
+ ```typescript
334
+ // server/index.ts
335
+ import { Server } from 'socket.io';
336
+ import { GameServer, Entity } from '@gamerstake/game-core';
337
+ import { TagGameRules } from './TagGameRules.js';
338
+
339
+ const io = new Server(3000, {
340
+ cors: { origin: '*' },
341
+ });
342
+
343
+ const gameServer = new GameServer();
344
+ gameServer.setServer(io);
345
+
346
+ const room = gameServer.createRoom('tag-game', new TagGameRules(), {
347
+ tickRate: 20,
348
+ cellSize: 256,
349
+ });
350
+
351
+ io.on('connection', (socket) => {
352
+ const playerId = socket.id;
353
+
354
+ // Create player
355
+ const player = new Entity(playerId, 0, 0) as any;
356
+ player.isIt = false;
357
+ room.addPlayer(player);
358
+
359
+ // Register socket
360
+ room.getNetwork().registerSocket(playerId, socket);
361
+
362
+ // Send initial state
363
+ socket.emit('S_INIT', {
364
+ playerId,
365
+ ...room.getSnapshot(),
366
+ });
367
+
368
+ // Handle inputs
369
+ socket.on('C_MOVE', (data) => {
370
+ room.queueInput(playerId, {
371
+ seq: data.seq,
372
+ type: 'move',
373
+ dir: data.dir,
374
+ timestamp: Date.now(),
375
+ });
376
+ });
377
+
378
+ socket.on('C_STOP', (data) => {
379
+ room.queueInput(playerId, {
380
+ seq: data.seq,
381
+ type: 'stop',
382
+ timestamp: Date.now(),
383
+ });
384
+ });
385
+
386
+ socket.on('disconnect', () => {
387
+ room.removePlayer(playerId);
388
+ room.getNetwork().unregisterSocket(playerId);
389
+ });
390
+ });
391
+
392
+ console.log(' Tag game server running on port 3000');
393
+ ```
394
+
395
+ ### Step 3: Create the Client
396
+
397
+ ```typescript
398
+ // client/index.ts
399
+ import { io } from 'socket.io-client';
400
+
401
+ const socket = io('http://localhost:3000');
402
+ let seq = 0;
403
+ let playerId: string;
404
+ let entities = new Map();
405
+
406
+ // Receive initial state
407
+ socket.on('S_INIT', (data) => {
408
+ playerId = data.playerId;
409
+ data.entities.forEach((e) => entities.set(e.id, e));
410
+ render();
411
+ });
412
+
413
+ // Receive updates
414
+ socket.on('S_UPDATE', (data) => {
415
+ data.entities?.forEach((e) => entities.set(e.id, e));
416
+ data.deleted?.forEach((id) => entities.delete(id));
417
+ render();
418
+ });
419
+
420
+ // Game events
421
+ socket.on('TAGGED', (data) => {
422
+ console.log(`Player ${data.tagged} was tagged by ${data.tagger}!`);
423
+ });
424
+
425
+ // Send movement input
426
+ function move(dirX: number, dirY: number) {
427
+ socket.emit('C_MOVE', {
428
+ seq: ++seq,
429
+ dir: { x: dirX, y: dirY },
430
+ });
431
+ }
432
+
433
+ function stop() {
434
+ socket.emit('C_STOP', { seq: ++seq });
435
+ }
436
+
437
+ // Keyboard controls
438
+ document.addEventListener('keydown', (e) => {
439
+ switch (e.key) {
440
+ case 'w':
441
+ move(0, -1);
442
+ break;
443
+ case 's':
444
+ move(0, 1);
445
+ break;
446
+ case 'a':
447
+ move(-1, 0);
448
+ break;
449
+ case 'd':
450
+ move(1, 0);
451
+ break;
452
+ }
453
+ });
454
+
455
+ document.addEventListener('keyup', () => stop());
456
+
457
+ // Simple canvas rendering
458
+ function render() {
459
+ const canvas = document.getElementById('game') as HTMLCanvasElement;
460
+ const ctx = canvas.getContext('2d')!;
461
+
462
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
463
+
464
+ entities.forEach((entity, id) => {
465
+ // Draw player
466
+ ctx.fillStyle = entity.isIt ? 'red' : 'blue';
467
+ ctx.beginPath();
468
+ ctx.arc(entity.x * 0.5, entity.y * 0.5, 10, 0, Math.PI * 2);
469
+ ctx.fill();
470
+
471
+ // Highlight your player
472
+ if (id === playerId) {
473
+ ctx.strokeStyle = 'yellow';
474
+ ctx.lineWidth = 3;
475
+ ctx.stroke();
476
+ }
477
+ });
478
+ }
479
+ ```
480
+
481
+ ### Step 4: Run It!
482
+
483
+ ```bash
484
+ # Terminal 1: Start server
485
+ node server/index.js
486
+
487
+ # Terminal 2: Start client dev server
488
+ npm run dev
489
+
490
+ # Open http://localhost:5173 in multiple browser tabs
491
+ ```
492
+
493
+ **Congratulations! You've built your first multiplayer game!**
494
+
495
+ ---
496
+
497
+ ## Advanced Topics
498
+
499
+ ### Custom Entity Types
500
+
501
+ Extend `Entity` to add game-specific properties:
502
+
503
+ ```typescript
504
+ class Warrior extends Entity {
505
+ health = 100;
506
+ mana = 50;
507
+ inventory: Item[] = [];
508
+
509
+ takeDamage(amount: number) {
510
+ this.health -= amount;
511
+ this.markDirty(); // Important!
512
+ }
513
+
514
+ serialize() {
515
+ return {
516
+ ...super.serialize(),
517
+ health: this.health,
518
+ mana: this.mana,
519
+ };
520
+ }
521
+ }
522
+ ```
523
+
524
+ **Important:** Call `markDirty()` when you modify properties so they're broadcast!
525
+
526
+ ### Collision Detection
527
+
528
+ Use the built-in AABB system:
529
+
530
+ ```typescript
531
+ import { AABBCollision, AABB } from '@gamerstake/game-core';
532
+
533
+ const collision = new AABBCollision();
534
+
535
+ const a: AABB = { x: 10, y: 10, width: 50, height: 50 };
536
+ const b: AABB = { x: 40, y: 40, width: 50, height: 50 };
537
+
538
+ if (collision.check(a, b)) {
539
+ console.log('Collision detected!');
540
+ }
541
+ ```
542
+
543
+ ### Spatial Queries
544
+
545
+ Find entities near a point efficiently:
546
+
547
+ ```typescript
548
+ onTick(room: Room, delta: number) {
549
+ const player = room.getRegistry().get('player-1');
550
+
551
+ // Find all entities within 200 units
552
+ const nearby = room.getGrid().getNearbyEntities(
553
+ player.x,
554
+ player.y,
555
+ 200
556
+ );
557
+
558
+ nearby.forEach(entity => {
559
+ // Check for interactions
560
+ });
561
+ }
562
+ ```
563
+
564
+ ### Custom Commands
565
+
566
+ Define your own command types:
567
+
568
+ ```typescript
569
+ // shared/commands.ts
570
+ interface AttackCommand extends Command {
571
+ type: 'attack';
572
+ targetId: string;
573
+ }
574
+
575
+ // server/GameRules.ts
576
+ onCommand(room: Room, playerId: string, command: Command) {
577
+ if (command.type === 'attack') {
578
+ const cmd = command as AttackCommand;
579
+ const attacker = room.getRegistry().get(playerId);
580
+ const target = room.getRegistry().get(cmd.targetId);
581
+
582
+ if (attacker && target) {
583
+ // Process attack
584
+ target.health -= attacker.attackPower;
585
+ target.markDirty();
586
+ }
587
+ }
588
+ }
589
+ ```
590
+
591
+ ### State Persistence
592
+
593
+ Save and restore room state:
594
+
595
+ ```typescript
596
+ // Save state
597
+ const snapshot = room.getSnapshot();
598
+ await db.saveGameState(roomId, snapshot);
599
+
600
+ // Load state
601
+ const savedState = await db.loadGameState(roomId);
602
+ savedState.entities.forEach((data) => {
603
+ const entity = new Entity(data.id, data.x, data.y);
604
+ // Restore other properties
605
+ room.spawnEntity(entity);
606
+ });
607
+ ```
608
+
609
+ ### Multiple Rooms
610
+
611
+ Create different room types:
612
+
613
+ ```typescript
614
+ // Lobby room (persistent)
615
+ const lobby = gameServer.createRoom('lobby', new LobbyRules(), {
616
+ tickRate: 10, // Lower rate for lobby
617
+ });
618
+
619
+ // Match room (temporary)
620
+ function createMatch(players: string[]) {
621
+ const matchId = `match-${Date.now()}`;
622
+ const match = gameServer.createRoom(matchId, new BattleRules(), {
623
+ tickRate: 20,
624
+ });
625
+
626
+ players.forEach((playerId) => {
627
+ // Transfer player from lobby to match
628
+ });
629
+
630
+ return match;
631
+ }
632
+ ```
633
+
634
+ ### Visibility/Interest Management
635
+
636
+ Only send updates about nearby entities:
637
+
638
+ ```typescript
639
+ onTick(room: Room, delta: number) {
640
+ const snapshot = room.getSnapshot();
641
+
642
+ // Send custom snapshots per player
643
+ room.getRegistry().forEach((player) => {
644
+ const nearby = room.getGrid().getNearbyEntities(
645
+ player.x,
646
+ player.y,
647
+ 500 // Visibility range
648
+ );
649
+
650
+ const visibleEntities = snapshot.entities.filter(e =>
651
+ nearby.has(e.id) || e.id === player.id
652
+ );
653
+
654
+ room.sendTo(player.id, {
655
+ op: 'S_UPDATE',
656
+ entities: visibleEntities,
657
+ });
658
+ });
659
+ }
660
+ ```
661
+
662
+ ---
663
+
664
+ ## Performance Optimization
665
+
666
+ ### 1. Avoid Allocations in Hot Paths
667
+
668
+ **Bad:**
669
+
670
+ ```typescript
671
+ onTick(room: Room, delta: number) {
672
+ const players = room.getRegistry().values(); // Allocates!
673
+ Array.from(players).forEach(...); // Allocates!
674
+ }
675
+ ```
676
+
677
+ **Good:**
678
+
679
+ ```typescript
680
+ onTick(room: Room, delta: number) {
681
+ room.getRegistry().forEach((player) => {
682
+ // Direct iteration, no allocation
683
+ });
684
+ }
685
+ ```
686
+
687
+ ### 2. Use Dirty Flags
688
+
689
+ Only process/broadcast changed entities:
690
+
691
+ ```typescript
692
+ onTick(room: Room, delta: number) {
693
+ room.getRegistry().forEach((entity) => {
694
+ if (entity.dirty) {
695
+ // This entity changed, process it
696
+ }
697
+ });
698
+ }
699
+ ```
700
+
701
+ ### 3. Batch Operations
702
+
703
+ Process similar entities together:
704
+
705
+ ```typescript
706
+ // Good: Process all projectiles at once
707
+ const projectiles = Array.from(room.getRegistry().values()).filter((e) => e.type === 'projectile');
708
+
709
+ projectiles.forEach((p) => p.updatePosition(delta));
710
+ ```
711
+
712
+ ### 4. Use Grid for Collision Detection
713
+
714
+ **Bad - O(n²):**
715
+
716
+ ```typescript
717
+ const entities = Array.from(room.getRegistry().values());
718
+ for (const a of entities) {
719
+ for (const b of entities) {
720
+ if (checkCollision(a, b)) { ... }
721
+ }
722
+ }
723
+ ```
724
+
725
+ **Good - O(n):**
726
+
727
+ ```typescript
728
+ room.getRegistry().forEach((entity) => {
729
+ const nearby = room.getGrid().getNearbyEntities(
730
+ entity.x,
731
+ entity.y,
732
+ entity.collisionRadius * 2
733
+ );
734
+
735
+ nearby.forEach(other => {
736
+ if (checkCollision(entity, other)) { ... }
737
+ });
738
+ });
739
+ ```
740
+
741
+ ### 5. Monitor Tick Performance
742
+
743
+ ```typescript
744
+ const metrics = gameServer.getMetrics();
745
+ metrics.rooms.forEach((room) => {
746
+ if (room.avgTickTime > 40) {
747
+ console.warn(`Room ${room.id} tick time too high: ${room.avgTickTime}ms`);
748
+ }
749
+ });
750
+ ```
751
+
752
+ **Target:** Keep average tick time below 40ms (80% of 50ms budget)
753
+
754
+ ---
755
+
756
+ ## Common Patterns
757
+
758
+ ### Pattern 1: Ability System
759
+
760
+ ```typescript
761
+ interface Ability {
762
+ cooldown: number;
763
+ lastUsed: number;
764
+ execute(caster: Entity, target?: Entity): void;
765
+ }
766
+
767
+ class FireballAbility implements Ability {
768
+ cooldown = 5000; // 5 seconds
769
+ lastUsed = 0;
770
+
771
+ execute(caster: Entity, target?: Entity) {
772
+ const now = Date.now();
773
+ if (now - this.lastUsed < this.cooldown) {
774
+ return; // Still on cooldown
775
+ }
776
+
777
+ this.lastUsed = now;
778
+
779
+ // Create projectile
780
+ const projectile = new Entity(`fireball-${now}`, caster.x, caster.y);
781
+ // ... set velocity toward target
782
+ }
783
+ }
784
+ ```
785
+
786
+ ### Pattern 2: State Machine
787
+
788
+ ```typescript
789
+ enum GameState {
790
+ WAITING,
791
+ COUNTDOWN,
792
+ PLAYING,
793
+ ENDED,
794
+ }
795
+
796
+ class MatchRules implements GameRules {
797
+ private state = GameState.WAITING;
798
+ private countdown = 0;
799
+
800
+ onTick(room: Room, delta: number) {
801
+ switch (this.state) {
802
+ case GameState.WAITING:
803
+ if (room.getRegistry().size >= 2) {
804
+ this.state = GameState.COUNTDOWN;
805
+ this.countdown = 5000;
806
+ }
807
+ break;
808
+
809
+ case GameState.COUNTDOWN:
810
+ this.countdown -= delta;
811
+ if (this.countdown <= 0) {
812
+ this.state = GameState.PLAYING;
813
+ room.broadcast({ op: 'GAME_START' });
814
+ }
815
+ break;
816
+
817
+ case GameState.PLAYING:
818
+ // Game logic here
819
+ if (this.checkWinCondition(room)) {
820
+ this.state = GameState.ENDED;
821
+ }
822
+ break;
823
+
824
+ case GameState.ENDED:
825
+ // Cleanup
826
+ break;
827
+ }
828
+ }
829
+ }
830
+ ```
831
+
832
+ ### Pattern 3: Event System
833
+
834
+ ```typescript
835
+ type GameEvent = { type: 'player_died'; playerId: string } | { type: 'item_collected'; itemId: string; playerId: string } | { type: 'match_ended'; winner: string };
836
+
837
+ class EventBus {
838
+ private listeners = new Map<string, ((event: GameEvent) => void)[]>();
839
+
840
+ on(type: string, handler: (event: GameEvent) => void) {
841
+ if (!this.listeners.has(type)) {
842
+ this.listeners.set(type, []);
843
+ }
844
+ this.listeners.get(type)!.push(handler);
845
+ }
846
+
847
+ emit(event: GameEvent) {
848
+ const handlers = this.listeners.get(event.type) || [];
849
+ handlers.forEach((h) => h(event));
850
+ }
851
+ }
852
+
853
+ // Usage in GameRules
854
+ class MyRules implements GameRules {
855
+ private events = new EventBus();
856
+
857
+ constructor() {
858
+ this.events.on('player_died', (event) => {
859
+ // Award kill to attacker
860
+ // Respawn player
861
+ });
862
+ }
863
+ }
864
+ ```
865
+
866
+ ### Pattern 4: Component System (ECS-lite)
867
+
868
+ ```typescript
869
+ interface Component {
870
+ update?(delta: number): void;
871
+ }
872
+
873
+ class HealthComponent implements Component {
874
+ constructor(
875
+ public max: number,
876
+ public current: number,
877
+ ) {}
878
+
879
+ takeDamage(amount: number) {
880
+ this.current = Math.max(0, this.current - amount);
881
+ }
882
+ }
883
+
884
+ class GameObject extends Entity {
885
+ private components = new Map<string, Component>();
886
+
887
+ addComponent(name: string, component: Component) {
888
+ this.components.set(name, component);
889
+ }
890
+
891
+ getComponent<T>(name: string): T | undefined {
892
+ return this.components.get(name) as T;
893
+ }
894
+
895
+ update(delta: number) {
896
+ this.components.forEach((c) => c.update?.(delta));
897
+ }
898
+ }
899
+ ```
900
+
901
+ ---
902
+
903
+ ## Troubleshooting
904
+
905
+ ### Problem: Tick time too high (>50ms)
906
+
907
+ **Solutions:**
908
+
909
+ - Profile with `console.time()` to find slow code
910
+ - Use grid for spatial queries
911
+ - Reduce entity count
912
+ - Optimize collision detection
913
+ - Use dirty flags
914
+
915
+ ### Problem: State desync between client/server
916
+
917
+ **Solutions:**
918
+
919
+ - Always trust the server
920
+ - Implement client-side prediction
921
+ - Send position corrections from server
922
+ - Add sequence numbers to commands
923
+
924
+ ### Problem: Network lag
925
+
926
+ **Solutions:**
927
+
928
+ - Implement client-side prediction
929
+ - Use delta compression
930
+ - Reduce broadcast frequency for distant entities
931
+ - Add interest management
932
+
933
+ ### Problem: Memory leaks
934
+
935
+ **Solutions:**
936
+
937
+ - Call `room.destroyEntity()` to remove entities
938
+ - Unregister sockets on disconnect
939
+ - Clear intervals/timeouts
940
+ - Destroy rooms when empty
941
+
942
+ ---
943
+
944
+ ## Best Practices
945
+
946
+ ### DO
947
+
948
+ - **Keep tick time under 40ms** (80% of 50ms budget)
949
+ - **Use dirty flags** to minimize broadcasts
950
+ - **Validate all client inputs** (never trust the client!)
951
+ - **Use spatial grid** for collision/visibility queries
952
+ - **Log metrics** for monitoring
953
+ - **Implement reconnection logic** for clients
954
+ - **Version your protocol** for updates
955
+ - **Test with multiple clients** regularly
956
+
957
+ ### DON'T
958
+
959
+ - **Don't allocate in hot paths** (onTick, etc.)
960
+ - **Don't trust client positions** (validate server-side)
961
+ - **Don't do O(n²) operations** every tick
962
+ - **Don't forget to call markDirty()** on entity changes
963
+ - **Don't broadcast to all players** if unnecessary
964
+ - **Don't block the tick loop** (use async wisely)
965
+ - **Don't forget error handling** (socket disconnects, etc.)
966
+
967
+ ---
968
+
969
+ ## Next Steps
970
+
971
+ Now that you understand game-core, you can:
972
+
973
+ 1. **Build your game** - Start with the tag game example and expand
974
+ 2. **Add a client UI** - Use Phaser, Three.js, or canvas
975
+ 3. **Implement game mechanics** - Combat, inventory, progression
976
+ 4. **Add persistence** - Save game state to database
977
+ 5. **Deploy** - Docker, Kubernetes, cloud hosting
978
+ 6. **Monitor** - Add logging, metrics, alerting
979
+
980
+ ### Resources
981
+
982
+ - **Examples:** `/examples/` directory
983
+ - **Tests:** `/tests/` for usage patterns
984
+ - **API Docs:** See README.md
985
+ - **Architecture:** See RFC documents
986
+
987
+ ### Need Help?
988
+
989
+ - Check the examples directory
990
+ - Read the test files for usage patterns
991
+ - Review the source code (it's well-documented!)
992
+ - Contact the GamerStake engineering team
993
+
994
+ ---
995
+
996
+ **Happy game building!**