@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.
- package/.eslintrc.json +22 -0
- package/.testing-guide-summary.md +261 -0
- package/DEVELOPER_GUIDE.md +996 -0
- package/MANUAL_TESTING.md +369 -0
- package/QUICK_START.md +368 -0
- package/README.md +379 -0
- package/TESTING_OVERVIEW.md +378 -0
- package/dist/index.d.ts +1266 -0
- package/dist/index.js +1632 -0
- package/dist/index.js.map +1 -0
- package/examples/simple-game/README.md +176 -0
- package/examples/simple-game/client.ts +201 -0
- package/examples/simple-game/package.json +14 -0
- package/examples/simple-game/server.ts +233 -0
- package/jest.config.ts +39 -0
- package/package.json +54 -0
- package/src/core/GameLoop.ts +214 -0
- package/src/core/GameRules.ts +103 -0
- package/src/core/GameServer.ts +200 -0
- package/src/core/Room.ts +368 -0
- package/src/entities/Entity.ts +118 -0
- package/src/entities/Registry.ts +161 -0
- package/src/index.ts +51 -0
- package/src/input/Command.ts +41 -0
- package/src/input/InputQueue.ts +130 -0
- package/src/network/Network.ts +112 -0
- package/src/network/Snapshot.ts +59 -0
- package/src/physics/AABB.ts +104 -0
- package/src/physics/Movement.ts +124 -0
- package/src/spatial/Grid.ts +202 -0
- package/src/types/index.ts +117 -0
- package/src/types/protocol.ts +161 -0
- package/src/utils/Logger.ts +112 -0
- package/src/utils/RingBuffer.ts +116 -0
- package/tests/AABB.test.ts +38 -0
- package/tests/Entity.test.ts +35 -0
- package/tests/GameLoop.test.ts +58 -0
- package/tests/GameServer.test.ts +64 -0
- package/tests/Grid.test.ts +28 -0
- package/tests/InputQueue.test.ts +42 -0
- package/tests/Movement.test.ts +37 -0
- package/tests/Network.test.ts +39 -0
- package/tests/Registry.test.ts +36 -0
- package/tests/RingBuffer.test.ts +38 -0
- package/tests/Room.test.ts +80 -0
- package/tests/Snapshot.test.ts +19 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# @gamerstake/game-core
|
|
2
|
+
|
|
3
|
+
Reusable multiplayer game engine for the GamerStake platform.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`game-core` is a battle-tested, high-performance multiplayer game engine extracted from the GamerStake metaverse. It provides:
|
|
8
|
+
|
|
9
|
+
- **Game-agnostic multiplayer infrastructure** (tick loop, networking, state management)
|
|
10
|
+
- **Pluggable game rules** via the `GameRules` interface
|
|
11
|
+
- **Room/Match lifecycle** management (persistent worlds OR match-based games)
|
|
12
|
+
- **Spatial partitioning** with O(1) queries via grid system
|
|
13
|
+
- **Basic 2D physics** (position, velocity, AABB collision)
|
|
14
|
+
- **Zero-allocation hot paths** for optimal performance
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
### Core Systems
|
|
19
|
+
|
|
20
|
+
- **GameServer** - Multi-room server management
|
|
21
|
+
- **Room** - Single game instance with tick loop
|
|
22
|
+
- **GameLoop** - Fixed 20 TPS with drift compensation
|
|
23
|
+
- **Entity System** - Base entity class with dirty flag tracking
|
|
24
|
+
- **Registry** - Entity lifecycle management
|
|
25
|
+
|
|
26
|
+
### Spatial & Physics
|
|
27
|
+
|
|
28
|
+
- **Grid** - Spatial partitioning for efficient queries
|
|
29
|
+
- **AABB Collision** - Bounding box collision detection
|
|
30
|
+
- **Movement** - Velocity integration and boundary constraints
|
|
31
|
+
|
|
32
|
+
### Networking
|
|
33
|
+
|
|
34
|
+
- **Network Layer** - Socket.io abstraction with opcodes
|
|
35
|
+
- **Snapshot System** - Full and delta state synchronization
|
|
36
|
+
- **InputQueue** - Buffered input processing with RingBuffer
|
|
37
|
+
|
|
38
|
+
### Performance
|
|
39
|
+
|
|
40
|
+
- **Zero-allocation buffers** - Reused arrays in hot paths
|
|
41
|
+
- **Dirty flag tracking** - Only broadcast changed entities
|
|
42
|
+
- **RingBuffer** - O(1) push/pop for input queues
|
|
43
|
+
- **Performance metrics** - Real-time tick time monitoring
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
### From Monorepo Workspace
|
|
48
|
+
|
|
49
|
+
If you're in the GamerStake monorepo:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@gamerstake/game-core": "workspace:*"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### From NPM Registry
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pnpm add @gamerstake/game-core
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
See [PUBLISHING.md](./PUBLISHING.md) for publishing and distribution options.
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
### 1. Implement GameRules
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { GameRules, Room, Entity, MoveCommand } from '@gamerstake/game-core';
|
|
73
|
+
|
|
74
|
+
class MyGameRules implements GameRules {
|
|
75
|
+
onRoomCreated(room: Room): void {
|
|
76
|
+
console.log('Room created!');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onPlayerJoin(room: Room, player: Entity): void {
|
|
80
|
+
console.log(`Player ${player.id} joined`);
|
|
81
|
+
// Spawn player at random position
|
|
82
|
+
player.setPosition(Math.random() * 1000, Math.random() * 1000);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
onPlayerLeave(room: Room, playerId: string): void {
|
|
86
|
+
console.log(`Player ${playerId} left`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
onTick(room: Room, delta: number): void {
|
|
90
|
+
// Update all entities
|
|
91
|
+
room.getRegistry().forEach((entity) => {
|
|
92
|
+
entity.updatePosition(delta);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onCommand(room: Room, playerId: string, command: Command): void {
|
|
97
|
+
const player = room.getRegistry().get(playerId);
|
|
98
|
+
if (!player) return;
|
|
99
|
+
|
|
100
|
+
if (command.type === 'move') {
|
|
101
|
+
const moveCmd = command as MoveCommand;
|
|
102
|
+
// Set velocity (e.g., 200 units/second)
|
|
103
|
+
const speed = 200;
|
|
104
|
+
player.setVelocity(moveCmd.dir.x * speed, moveCmd.dir.y * speed);
|
|
105
|
+
} else if (command.type === 'stop') {
|
|
106
|
+
player.setVelocity(0, 0);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
shouldEndRoom(room: Room): boolean {
|
|
111
|
+
// For persistent worlds, return false
|
|
112
|
+
// For match-based games, check win condition
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2. Create Server and Room
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { GameServer, Entity } from '@gamerstake/game-core';
|
|
122
|
+
import { Server } from 'socket.io';
|
|
123
|
+
|
|
124
|
+
// Create Socket.io server
|
|
125
|
+
const io = new Server(3000);
|
|
126
|
+
|
|
127
|
+
// Create game server
|
|
128
|
+
const gameServer = new GameServer();
|
|
129
|
+
gameServer.setServer(io);
|
|
130
|
+
|
|
131
|
+
// Create a room
|
|
132
|
+
const room = gameServer.createRoom('room-1', new MyGameRules(), {
|
|
133
|
+
tickRate: 20, // 20 ticks per second
|
|
134
|
+
cellSize: 512, // Grid cell size
|
|
135
|
+
maxEntities: 1000, // Max entities per room
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Handle player connections
|
|
139
|
+
io.on('connection', (socket) => {
|
|
140
|
+
const playerId = socket.id;
|
|
141
|
+
|
|
142
|
+
// Create player entity
|
|
143
|
+
const player = new Entity(playerId, 0, 0);
|
|
144
|
+
room.addPlayer(player);
|
|
145
|
+
|
|
146
|
+
// Register socket for networking
|
|
147
|
+
room.getNetwork().registerSocket(playerId, socket);
|
|
148
|
+
|
|
149
|
+
// Send initial state
|
|
150
|
+
room.sendTo(playerId, {
|
|
151
|
+
op: 'S_INIT',
|
|
152
|
+
playerId,
|
|
153
|
+
...room.getSnapshot(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Handle player inputs
|
|
157
|
+
socket.on('C_MOVE', (data) => {
|
|
158
|
+
room.queueInput(playerId, {
|
|
159
|
+
seq: data.seq,
|
|
160
|
+
type: 'move',
|
|
161
|
+
dir: data.dir,
|
|
162
|
+
timestamp: Date.now(),
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
socket.on('disconnect', () => {
|
|
167
|
+
room.removePlayer(playerId);
|
|
168
|
+
room.getNetwork().unregisterSocket(playerId);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
console.log('Game server running on port 3000');
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 3. Client Example
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { io } from 'socket.io-client';
|
|
179
|
+
|
|
180
|
+
const socket = io('http://localhost:3000');
|
|
181
|
+
|
|
182
|
+
let seq = 0;
|
|
183
|
+
|
|
184
|
+
socket.on('S_INIT', (data) => {
|
|
185
|
+
console.log('Initial state:', data);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
socket.on('S_UPDATE', (data) => {
|
|
189
|
+
console.log('Delta update:', data);
|
|
190
|
+
// Update local entities
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Send movement input
|
|
194
|
+
function move(dirX: number, dirY: number) {
|
|
195
|
+
socket.emit('C_MOVE', {
|
|
196
|
+
seq: ++seq,
|
|
197
|
+
dir: { x: dirX, y: dirY },
|
|
198
|
+
timestamp: Date.now(),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Example: Move right
|
|
203
|
+
move(1, 0);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Architecture
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
GameServer
|
|
210
|
+
Room 1
|
|
211
|
+
GameLoop (20 TPS)
|
|
212
|
+
Registry (Entities)
|
|
213
|
+
Grid (Spatial)
|
|
214
|
+
InputQueue
|
|
215
|
+
Network
|
|
216
|
+
GameRules (Your logic)
|
|
217
|
+
Room 2
|
|
218
|
+
Room N...
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## API Reference
|
|
222
|
+
|
|
223
|
+
### GameServer
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const server = new GameServer();
|
|
227
|
+
server.setServer(io);
|
|
228
|
+
const room = server.createRoom(id, rules, config);
|
|
229
|
+
server.destroyRoom(id);
|
|
230
|
+
const metrics = server.getMetrics();
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Room
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
room.start();
|
|
237
|
+
room.stop();
|
|
238
|
+
room.addPlayer(entity);
|
|
239
|
+
room.removePlayer(playerId);
|
|
240
|
+
room.spawnEntity(entity);
|
|
241
|
+
room.destroyEntity(entityId);
|
|
242
|
+
room.queueInput(playerId, command);
|
|
243
|
+
room.broadcast(event);
|
|
244
|
+
room.sendTo(playerId, event);
|
|
245
|
+
const snapshot = room.getSnapshot();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Entity
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
const entity = new Entity(id, x, y);
|
|
252
|
+
entity.setPosition(x, y);
|
|
253
|
+
entity.setVelocity(vx, vy);
|
|
254
|
+
entity.updatePosition(deltaMs);
|
|
255
|
+
entity.markDirty();
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Grid
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const grid = new Grid(cellSize);
|
|
262
|
+
grid.addEntity(id, x, y);
|
|
263
|
+
grid.removeEntity(id);
|
|
264
|
+
grid.moveEntity(id, oldX, oldY, newX, newY);
|
|
265
|
+
const nearby = grid.getNearbyEntities(x, y, range);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Configuration
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
interface RoomConfig {
|
|
272
|
+
tickRate?: number; // Default: 20 TPS
|
|
273
|
+
cellSize?: number; // Default: 512 units
|
|
274
|
+
maxInputQueueSize?: number; // Default: 100
|
|
275
|
+
maxEntities?: number; // Default: 1000
|
|
276
|
+
visibilityRange?: number; // Default: 1 (3x3 cells)
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Performance
|
|
281
|
+
|
|
282
|
+
### Benchmarks
|
|
283
|
+
|
|
284
|
+
- **20+ TPS** with 100+ entities per room
|
|
285
|
+
- **<40ms** average tick time (80% budget)
|
|
286
|
+
- **O(1)** spatial queries via grid partitioning
|
|
287
|
+
- **Zero allocations** in hot paths (tick loop)
|
|
288
|
+
|
|
289
|
+
### Optimizations
|
|
290
|
+
|
|
291
|
+
1. **Dirty Flag Tracking** - Only broadcast changed entities
|
|
292
|
+
2. **Buffer Reuse** - Pre-allocated arrays for queries
|
|
293
|
+
3. **RingBuffer** - O(1) input queue operations
|
|
294
|
+
4. **Spatial Grid** - Avoid O(n²) collision checks
|
|
295
|
+
|
|
296
|
+
## Testing
|
|
297
|
+
|
|
298
|
+
### Automated Tests
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Run unit tests
|
|
302
|
+
pnpm test
|
|
303
|
+
|
|
304
|
+
# Run with coverage
|
|
305
|
+
pnpm test:coverage
|
|
306
|
+
|
|
307
|
+
# Run in watch mode
|
|
308
|
+
pnpm test:watch
|
|
309
|
+
|
|
310
|
+
# Type check
|
|
311
|
+
pnpm typecheck
|
|
312
|
+
|
|
313
|
+
# Build
|
|
314
|
+
pnpm build
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Manual Testing
|
|
318
|
+
|
|
319
|
+
To manually test the game engine with a live server:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# 1. Build the package
|
|
323
|
+
pnpm build
|
|
324
|
+
|
|
325
|
+
# 2. Start the test server
|
|
326
|
+
node examples/simple-game/server.js
|
|
327
|
+
|
|
328
|
+
# 3. In another terminal, start a client
|
|
329
|
+
node examples/simple-game/client.js
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
See `examples/simple-game/README.md` for detailed manual testing instructions.
|
|
333
|
+
|
|
334
|
+
## Getting Started
|
|
335
|
+
|
|
336
|
+
Choose your path:
|
|
337
|
+
|
|
338
|
+
- ** Quick Start** → See [QUICK_START.md](./QUICK_START.md) - Get running in 5 minutes
|
|
339
|
+
- ** Full Guide** → See [DEVELOPER_GUIDE.md](./DEVELOPER_GUIDE.md) - Complete tutorial
|
|
340
|
+
- ** Examples** → See `examples/` directory - Working code samples
|
|
341
|
+
|
|
342
|
+
## Examples
|
|
343
|
+
|
|
344
|
+
The `examples/` directory contains:
|
|
345
|
+
|
|
346
|
+
- `simple-game/` - Manual testing server & client with automated movement tests
|
|
347
|
+
|
|
348
|
+
## Migration from Metaverse Shard
|
|
349
|
+
|
|
350
|
+
If you're migrating from the metaverse shard:
|
|
351
|
+
|
|
352
|
+
1. Install `@gamerstake/game-core`
|
|
353
|
+
2. Implement `GameRules` with your metaverse logic
|
|
354
|
+
3. Replace direct `GameLoop`, `Grid`, `WorldState` usage
|
|
355
|
+
4. Keep metaverse-specific: persistence, zones, gateway
|
|
356
|
+
|
|
357
|
+
See `docs/features/game-core/N2-architecture/game-core-rfc.md` for details.
|
|
358
|
+
|
|
359
|
+
## Documentation
|
|
360
|
+
|
|
361
|
+
### For Developers
|
|
362
|
+
|
|
363
|
+
- **[Quick Start Guide](./QUICK_START.md)** - Get a multiplayer game running in 5 minutes
|
|
364
|
+
- **[Developer Guide](./DEVELOPER_GUIDE.md)** - Comprehensive guide with examples and patterns
|
|
365
|
+
- **[Manual Testing](./examples/simple-game/README.md)** - How to manually test the engine
|
|
366
|
+
- **[Publishing Guide](./PUBLISHING.md)** - How to publish and distribute the package
|
|
367
|
+
|
|
368
|
+
### Architecture
|
|
369
|
+
|
|
370
|
+
- [N1 Requirements (PRD)](../../docs/features/game-core/N1-requirements/game-core-prd.md)
|
|
371
|
+
- [N2 Architecture (RFC)](../../docs/features/game-core/N2-architecture/game-core-rfc.md)
|
|
372
|
+
|
|
373
|
+
## License
|
|
374
|
+
|
|
375
|
+
UNLICENSED - Internal GamerStake package
|
|
376
|
+
|
|
377
|
+
## Contributing
|
|
378
|
+
|
|
379
|
+
This is an internal package. For issues or feature requests, contact the GamerStake engineering team.
|