nostr-arena 0.1.1
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/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/__tests__/callbacks.test.d.ts +28 -0
- package/dist/__tests__/callbacks.test.d.ts.map +1 -0
- package/dist/__tests__/callbacks.test.js +140 -0
- package/dist/__tests__/callbacks.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +18 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +43 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/error-handling.test.d.ts +14 -0
- package/dist/__tests__/error-handling.test.d.ts.map +1 -0
- package/dist/__tests__/error-handling.test.js +199 -0
- package/dist/__tests__/error-handling.test.js.map +1 -0
- package/dist/__tests__/protocol.test.d.ts +7 -0
- package/dist/__tests__/protocol.test.d.ts.map +1 -0
- package/dist/__tests__/protocol.test.js +257 -0
- package/dist/__tests__/protocol.test.js.map +1 -0
- package/dist/__tests__/state-flow.test.d.ts +37 -0
- package/dist/__tests__/state-flow.test.d.ts.map +1 -0
- package/dist/__tests__/state-flow.test.js +160 -0
- package/dist/__tests__/state-flow.test.js.map +1 -0
- package/dist/__tests__/timing.test.d.ts +20 -0
- package/dist/__tests__/timing.test.d.ts.map +1 -0
- package/dist/__tests__/timing.test.js +73 -0
- package/dist/__tests__/timing.test.js.map +1 -0
- package/dist/core/Arena.d.ts +164 -0
- package/dist/core/Arena.d.ts.map +1 -0
- package/dist/core/Arena.js +634 -0
- package/dist/core/Arena.js.map +1 -0
- package/dist/core/NostrClient.d.ts +108 -0
- package/dist/core/NostrClient.d.ts.map +1 -0
- package/dist/core/NostrClient.js +225 -0
- package/dist/core/NostrClient.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +7 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy.d.ts +36 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +98 -0
- package/dist/proxy.js.map +1 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/useArena.d.ts +74 -0
- package/dist/react/useArena.d.ts.map +1 -0
- package/dist/react/useArena.js +224 -0
- package/dist/react/useArena.js.map +1 -0
- package/dist/retry.d.ts +54 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +82 -0
- package/dist/retry.js.map +1 -0
- package/dist/testing/MockArena.d.ts +84 -0
- package/dist/testing/MockArena.d.ts.map +1 -0
- package/dist/testing/MockArena.js +156 -0
- package/dist/testing/MockArena.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +6 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types.d.ts +183 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +55 -0
- package/dist/types.js.map +1 -0
- package/package.json +84 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Flow Tests
|
|
3
|
+
*
|
|
4
|
+
* Source: architecture.md - State Flow diagram
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* ┌─────────┐ create() ┌─────────┐ opponent ┌─────────┐
|
|
8
|
+
* │ idle │────────────▶│ waiting │────joins───▶│ ready │
|
|
9
|
+
* └─────────┘ └─────────┘ └────┬────┘
|
|
10
|
+
* ▲ │
|
|
11
|
+
* │ game starts
|
|
12
|
+
* │ │
|
|
13
|
+
* │ ┌──────────┐ ┌───▼─────┐
|
|
14
|
+
* │◀────leave()──────│ finished │◀──gameover──│ playing │
|
|
15
|
+
* │ └────┬─────┘ └─────────┘
|
|
16
|
+
* │ │
|
|
17
|
+
* │ rematch
|
|
18
|
+
* │ │
|
|
19
|
+
* │ ┌────▼────┐
|
|
20
|
+
* └──────────────────│ ready │
|
|
21
|
+
* └─────────┘
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Source: README.md - Room States table
|
|
25
|
+
*
|
|
26
|
+
* | Status | Description |
|
|
27
|
+
* | -------- | ------------------------------------- |
|
|
28
|
+
* | idle | No room active |
|
|
29
|
+
* | creating | Creating a new room |
|
|
30
|
+
* | waiting | Waiting for opponent to join |
|
|
31
|
+
* | joining | Joining an existing room |
|
|
32
|
+
* | ready | Both players connected, ready to play |
|
|
33
|
+
* | playing | Game in progress |
|
|
34
|
+
* | finished | Game ended |
|
|
35
|
+
*/
|
|
36
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
37
|
+
import { MockArena } from '../testing/MockArena';
|
|
38
|
+
describe('State Flow (architecture.md)', () => {
|
|
39
|
+
let room;
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
room = new MockArena({ gameId: 'test-game' });
|
|
42
|
+
});
|
|
43
|
+
describe('Initial state: idle', () => {
|
|
44
|
+
/**
|
|
45
|
+
* Source: README.md - "idle: No room active"
|
|
46
|
+
*/
|
|
47
|
+
it('should start in idle status', () => {
|
|
48
|
+
expect(room.roomState.status).toBe('idle');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('Transition: idle → waiting (via create())', () => {
|
|
52
|
+
/**
|
|
53
|
+
* Source: architecture.md diagram
|
|
54
|
+
* "idle ──create()──▶ waiting"
|
|
55
|
+
*/
|
|
56
|
+
it('create() should transition from idle to waiting', async () => {
|
|
57
|
+
expect(room.roomState.status).toBe('idle');
|
|
58
|
+
await room.create();
|
|
59
|
+
expect(room.roomState.status).toBe('waiting');
|
|
60
|
+
});
|
|
61
|
+
it('after create(), isHost should be true', async () => {
|
|
62
|
+
await room.create();
|
|
63
|
+
expect(room.roomState.isHost).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('Transition: waiting → ready (opponent joins)', () => {
|
|
67
|
+
/**
|
|
68
|
+
* Source: architecture.md diagram
|
|
69
|
+
* "waiting ──opponent joins──▶ ready"
|
|
70
|
+
*/
|
|
71
|
+
it('should transition from waiting to ready when opponent joins', async () => {
|
|
72
|
+
await room.create();
|
|
73
|
+
expect(room.roomState.status).toBe('waiting');
|
|
74
|
+
room.simulateOpponentJoin('opponent-pubkey');
|
|
75
|
+
expect(room.roomState.status).toBe('ready');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('Transition: idle → ready (via join())', () => {
|
|
79
|
+
/**
|
|
80
|
+
* Source: architecture.md - Arena API
|
|
81
|
+
* "join(roomId): Promise<void>"
|
|
82
|
+
*
|
|
83
|
+
* Note: The diagram shows idle → joining → ready, but for MockArena
|
|
84
|
+
* join() directly goes to ready (simplified for testing)
|
|
85
|
+
*/
|
|
86
|
+
it('join() should transition to ready', async () => {
|
|
87
|
+
expect(room.roomState.status).toBe('idle');
|
|
88
|
+
await room.join('existing-room-id');
|
|
89
|
+
expect(room.roomState.status).toBe('ready');
|
|
90
|
+
});
|
|
91
|
+
it('after join(), isHost should be false', async () => {
|
|
92
|
+
await room.join('existing-room-id');
|
|
93
|
+
expect(room.roomState.isHost).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('Transition: playing → finished (gameover)', () => {
|
|
97
|
+
/**
|
|
98
|
+
* Source: architecture.md diagram
|
|
99
|
+
* "playing ──gameover──▶ finished"
|
|
100
|
+
*/
|
|
101
|
+
it('sendGameOver() should transition to finished', async () => {
|
|
102
|
+
await room.create();
|
|
103
|
+
room.simulateOpponentJoin('opponent');
|
|
104
|
+
room.sendGameOver('win', 500);
|
|
105
|
+
expect(room.roomState.status).toBe('finished');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('Transition: any → idle (leave())', () => {
|
|
109
|
+
/**
|
|
110
|
+
* Source: architecture.md diagram
|
|
111
|
+
* "◀────leave()────"
|
|
112
|
+
*/
|
|
113
|
+
it('leave() from waiting should go to idle', async () => {
|
|
114
|
+
await room.create();
|
|
115
|
+
expect(room.roomState.status).toBe('waiting');
|
|
116
|
+
room.leave();
|
|
117
|
+
expect(room.roomState.status).toBe('idle');
|
|
118
|
+
});
|
|
119
|
+
it('leave() from ready should go to idle', async () => {
|
|
120
|
+
await room.create();
|
|
121
|
+
room.simulateOpponentJoin('opponent');
|
|
122
|
+
expect(room.roomState.status).toBe('ready');
|
|
123
|
+
room.leave();
|
|
124
|
+
expect(room.roomState.status).toBe('idle');
|
|
125
|
+
});
|
|
126
|
+
it('leave() from finished should go to idle', async () => {
|
|
127
|
+
await room.create();
|
|
128
|
+
room.simulateOpponentJoin('opponent');
|
|
129
|
+
room.sendGameOver('win');
|
|
130
|
+
expect(room.roomState.status).toBe('finished');
|
|
131
|
+
room.leave();
|
|
132
|
+
expect(room.roomState.status).toBe('idle');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('Transition: finished → ready (rematch)', () => {
|
|
136
|
+
/**
|
|
137
|
+
* Source: architecture.md diagram
|
|
138
|
+
* "finished ──rematch──▶ ready"
|
|
139
|
+
*/
|
|
140
|
+
it('acceptRematch() should transition from finished to ready', async () => {
|
|
141
|
+
await room.create();
|
|
142
|
+
room.simulateOpponentJoin('opponent');
|
|
143
|
+
room.sendGameOver('win');
|
|
144
|
+
expect(room.roomState.status).toBe('finished');
|
|
145
|
+
room.simulateRematchRequested();
|
|
146
|
+
room.acceptRematch();
|
|
147
|
+
expect(room.roomState.status).toBe('ready');
|
|
148
|
+
});
|
|
149
|
+
it('rematch should generate new seed', async () => {
|
|
150
|
+
await room.create();
|
|
151
|
+
room.simulateOpponentJoin('opponent');
|
|
152
|
+
const oldSeed = room.roomState.seed;
|
|
153
|
+
room.sendGameOver('win');
|
|
154
|
+
room.simulateRematchRequested();
|
|
155
|
+
room.acceptRematch();
|
|
156
|
+
expect(room.roomState.seed).not.toBe(oldSeed);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
//# sourceMappingURL=state-flow.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-flow.test.js","sourceRoot":"","sources":["../../src/__tests__/state-flow.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing Tests
|
|
3
|
+
*
|
|
4
|
+
* Source: architecture.md - Timing section
|
|
5
|
+
*
|
|
6
|
+
* | Constant | Default | Description |
|
|
7
|
+
* | --------------------- | ----------------- | --------------------- |
|
|
8
|
+
* | `roomExpiry` | 600000ms (10 min) | Room expiration |
|
|
9
|
+
* | `heartbeatInterval` | 3000ms | Heartbeat sending |
|
|
10
|
+
* | `disconnectThreshold` | 10000ms | Disconnect detection |
|
|
11
|
+
* | `stateThrottle` | 100ms | State update throttle |
|
|
12
|
+
* | `joinTimeout` | 30000ms | Join timeout |
|
|
13
|
+
*
|
|
14
|
+
* Source: protocol.md - Heartbeat section
|
|
15
|
+
*
|
|
16
|
+
* "Every 3 seconds"
|
|
17
|
+
* "If no heartbeat received for 10 seconds, opponent is considered disconnected."
|
|
18
|
+
*/
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=timing.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/timing.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing Tests
|
|
3
|
+
*
|
|
4
|
+
* Source: architecture.md - Timing section
|
|
5
|
+
*
|
|
6
|
+
* | Constant | Default | Description |
|
|
7
|
+
* | --------------------- | ----------------- | --------------------- |
|
|
8
|
+
* | `roomExpiry` | 600000ms (10 min) | Room expiration |
|
|
9
|
+
* | `heartbeatInterval` | 3000ms | Heartbeat sending |
|
|
10
|
+
* | `disconnectThreshold` | 10000ms | Disconnect detection |
|
|
11
|
+
* | `stateThrottle` | 100ms | State update throttle |
|
|
12
|
+
* | `joinTimeout` | 30000ms | Join timeout |
|
|
13
|
+
*
|
|
14
|
+
* Source: protocol.md - Heartbeat section
|
|
15
|
+
*
|
|
16
|
+
* "Every 3 seconds"
|
|
17
|
+
* "If no heartbeat received for 10 seconds, opponent is considered disconnected."
|
|
18
|
+
*/
|
|
19
|
+
import { describe, it, expect } from 'vitest';
|
|
20
|
+
import { DEFAULT_CONFIG } from '../types';
|
|
21
|
+
describe('Timing Constants (architecture.md)', () => {
|
|
22
|
+
describe('roomExpiry', () => {
|
|
23
|
+
it('should be 600000ms (10 min)', () => {
|
|
24
|
+
expect(DEFAULT_CONFIG.roomExpiry).toBe(600000);
|
|
25
|
+
});
|
|
26
|
+
it('should equal 10 minutes in milliseconds', () => {
|
|
27
|
+
const tenMinutesMs = 10 * 60 * 1000;
|
|
28
|
+
expect(DEFAULT_CONFIG.roomExpiry).toBe(tenMinutesMs);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('heartbeatInterval', () => {
|
|
32
|
+
it('should be 3000ms', () => {
|
|
33
|
+
expect(DEFAULT_CONFIG.heartbeatInterval).toBe(3000);
|
|
34
|
+
});
|
|
35
|
+
it('should equal 3 seconds in milliseconds', () => {
|
|
36
|
+
const threeSecondsMs = 3 * 1000;
|
|
37
|
+
expect(DEFAULT_CONFIG.heartbeatInterval).toBe(threeSecondsMs);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('disconnectThreshold', () => {
|
|
41
|
+
it('should be 10000ms', () => {
|
|
42
|
+
expect(DEFAULT_CONFIG.disconnectThreshold).toBe(10000);
|
|
43
|
+
});
|
|
44
|
+
it('should equal 10 seconds in milliseconds', () => {
|
|
45
|
+
const tenSecondsMs = 10 * 1000;
|
|
46
|
+
expect(DEFAULT_CONFIG.disconnectThreshold).toBe(tenSecondsMs);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('stateThrottle', () => {
|
|
50
|
+
it('should be 100ms', () => {
|
|
51
|
+
expect(DEFAULT_CONFIG.stateThrottle).toBe(100);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('Timing Relationships (protocol.md)', () => {
|
|
56
|
+
/**
|
|
57
|
+
* Source: protocol.md - Heartbeat section
|
|
58
|
+
*
|
|
59
|
+
* "Every 3 seconds ... If no heartbeat received for 10 seconds"
|
|
60
|
+
*
|
|
61
|
+
* This implies: disconnectThreshold > heartbeatInterval * 3
|
|
62
|
+
* (we need to miss at least 3 heartbeats to detect disconnect)
|
|
63
|
+
*/
|
|
64
|
+
it('should detect disconnect after missing ~3 heartbeats', () => {
|
|
65
|
+
const missedHeartbeats = DEFAULT_CONFIG.disconnectThreshold / DEFAULT_CONFIG.heartbeatInterval;
|
|
66
|
+
// 10000 / 3000 = 3.33, meaning ~3 missed heartbeats trigger disconnect
|
|
67
|
+
expect(missedHeartbeats).toBeGreaterThanOrEqual(3);
|
|
68
|
+
});
|
|
69
|
+
it('disconnect threshold should be greater than heartbeat interval', () => {
|
|
70
|
+
expect(DEFAULT_CONFIG.disconnectThreshold).toBeGreaterThan(DEFAULT_CONFIG.heartbeatInterval);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
//# sourceMappingURL=timing.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.test.js","sourceRoot":"","sources":["../../src/__tests__/timing.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1C,QAAQ,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAClD,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;QAC1B,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;YACrC,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;YACjD,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;YACpC,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACvD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;QACjC,EAAE,CAAC,kBAAkB,EAAE,GAAG,EAAE;YAC1B,MAAM,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,CAAC;YAChC,MAAM,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;QACnC,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;YAC3B,MAAM,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;YACjD,MAAM,YAAY,GAAG,EAAE,GAAG,IAAI,CAAC;YAC/B,MAAM,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,iBAAiB,EAAE,GAAG,EAAE;YACzB,MAAM,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAClD;;;;;;;OAOG;IAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,gBAAgB,GAAG,cAAc,CAAC,mBAAmB,GAAG,cAAc,CAAC,iBAAiB,CAAC;QAC/F,uEAAuE;QACvE,MAAM,CAAC,gBAAgB,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC,eAAe,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;IAC/F,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nostr-battle-room - Arena
|
|
3
|
+
* Main class for managing multiplayer game rooms over Nostr
|
|
4
|
+
*/
|
|
5
|
+
import type { ArenaConfig, RoomState, OpponentBase } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* Internal opponent state (mutable)
|
|
8
|
+
*/
|
|
9
|
+
interface InternalOpponentState<TGameState> extends OpponentBase {
|
|
10
|
+
gameState: TGameState | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Arena - Manages a multiplayer game room over Nostr
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* interface MyGameState {
|
|
18
|
+
* score: number;
|
|
19
|
+
* position: { x: number; y: number };
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* const room = new Arena<MyGameState>({
|
|
23
|
+
* gameId: 'my-game',
|
|
24
|
+
* relays: ['wss://relay.damus.io'],
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* room.on('opponentState', (state) => {
|
|
28
|
+
* console.log('Opponent score:', state.score);
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* const url = await room.create();
|
|
32
|
+
* console.log('Share this URL:', url);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class Arena<TGameState = Record<string, unknown>> {
|
|
36
|
+
private client;
|
|
37
|
+
private config;
|
|
38
|
+
private callbacks;
|
|
39
|
+
private _roomState;
|
|
40
|
+
private _opponent;
|
|
41
|
+
private unsubscribe;
|
|
42
|
+
private heartbeatInterval;
|
|
43
|
+
private disconnectCheckInterval;
|
|
44
|
+
private lastStateUpdate;
|
|
45
|
+
private storage;
|
|
46
|
+
constructor(config: ArenaConfig);
|
|
47
|
+
/** Current room state */
|
|
48
|
+
get roomState(): Readonly<RoomState>;
|
|
49
|
+
/** Current opponent state (null if no opponent) */
|
|
50
|
+
get opponent(): Readonly<InternalOpponentState<TGameState>> | null;
|
|
51
|
+
/** Whether connected to Nostr relays */
|
|
52
|
+
get isConnected(): boolean;
|
|
53
|
+
/** This player's public key */
|
|
54
|
+
get publicKey(): string;
|
|
55
|
+
/**
|
|
56
|
+
* Get the connection status of each relay
|
|
57
|
+
*/
|
|
58
|
+
getRelayStatus(): Map<string, boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* Check if at least one relay is connected
|
|
61
|
+
*/
|
|
62
|
+
get hasConnectedRelay(): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Register event callback for opponentJoin
|
|
65
|
+
*/
|
|
66
|
+
onOpponentJoin(callback: (publicKey: string) => void): this;
|
|
67
|
+
/**
|
|
68
|
+
* Register event callback for opponentState
|
|
69
|
+
*/
|
|
70
|
+
onOpponentState(callback: (state: TGameState) => void): this;
|
|
71
|
+
/**
|
|
72
|
+
* Register event callback for opponentDisconnect
|
|
73
|
+
*/
|
|
74
|
+
onOpponentDisconnect(callback: () => void): this;
|
|
75
|
+
/**
|
|
76
|
+
* Register event callback for opponentGameOver
|
|
77
|
+
*/
|
|
78
|
+
onOpponentGameOver(callback: (reason: string, finalScore?: number) => void): this;
|
|
79
|
+
/**
|
|
80
|
+
* Register event callback for rematchRequested
|
|
81
|
+
*/
|
|
82
|
+
onRematchRequested(callback: () => void): this;
|
|
83
|
+
/**
|
|
84
|
+
* Register event callback for rematchStart
|
|
85
|
+
*/
|
|
86
|
+
onRematchStart(callback: (newSeed: number) => void): this;
|
|
87
|
+
/**
|
|
88
|
+
* Register event callback for error
|
|
89
|
+
*/
|
|
90
|
+
onError(callback: (error: Error) => void): this;
|
|
91
|
+
/**
|
|
92
|
+
* Connect to Nostr relays
|
|
93
|
+
*/
|
|
94
|
+
connect(): void;
|
|
95
|
+
/**
|
|
96
|
+
* Disconnect from relays and clean up
|
|
97
|
+
*/
|
|
98
|
+
disconnect(): void;
|
|
99
|
+
/**
|
|
100
|
+
* Create a new room
|
|
101
|
+
*
|
|
102
|
+
* Times out after joinTimeout (default: 30 seconds)
|
|
103
|
+
* @returns Room URL (e.g., "https://example.com/battle/abc123")
|
|
104
|
+
*/
|
|
105
|
+
create(baseUrl?: string): Promise<string>;
|
|
106
|
+
/**
|
|
107
|
+
* Internal create implementation
|
|
108
|
+
*/
|
|
109
|
+
private createInternal;
|
|
110
|
+
/**
|
|
111
|
+
* Join an existing room
|
|
112
|
+
*
|
|
113
|
+
* Times out after joinTimeout (default: 30 seconds)
|
|
114
|
+
*/
|
|
115
|
+
join(roomId: string): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Internal join implementation
|
|
118
|
+
*/
|
|
119
|
+
private joinInternal;
|
|
120
|
+
/**
|
|
121
|
+
* Leave the current room
|
|
122
|
+
*/
|
|
123
|
+
leave(): void;
|
|
124
|
+
/**
|
|
125
|
+
* Attempt to reconnect to a previously joined room
|
|
126
|
+
*/
|
|
127
|
+
reconnect(): Promise<boolean>;
|
|
128
|
+
/**
|
|
129
|
+
* Internal reconnection logic
|
|
130
|
+
*/
|
|
131
|
+
private reconnectInternal;
|
|
132
|
+
/**
|
|
133
|
+
* Send game state to opponent (throttled)
|
|
134
|
+
*/
|
|
135
|
+
sendState(state: TGameState): void;
|
|
136
|
+
/**
|
|
137
|
+
* Send game over event
|
|
138
|
+
*/
|
|
139
|
+
sendGameOver(reason: string, finalScore?: number): void;
|
|
140
|
+
/**
|
|
141
|
+
* Request a rematch
|
|
142
|
+
*/
|
|
143
|
+
requestRematch(): void;
|
|
144
|
+
/**
|
|
145
|
+
* Accept a rematch request
|
|
146
|
+
*/
|
|
147
|
+
acceptRematch(): void;
|
|
148
|
+
private subscribeToRoom;
|
|
149
|
+
private handleRoomEvent;
|
|
150
|
+
private startHeartbeat;
|
|
151
|
+
private stopHeartbeat;
|
|
152
|
+
private startDisconnectCheck;
|
|
153
|
+
private stopDisconnectCheck;
|
|
154
|
+
private publishToRoom;
|
|
155
|
+
private createInitialOpponent;
|
|
156
|
+
private resetForRematch;
|
|
157
|
+
private get storageKey();
|
|
158
|
+
private saveRoom;
|
|
159
|
+
private loadRoom;
|
|
160
|
+
private clearRoom;
|
|
161
|
+
private isExpired;
|
|
162
|
+
}
|
|
163
|
+
export {};
|
|
164
|
+
//# sourceMappingURL=Arena.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Arena.d.ts","sourceRoot":"","sources":["../../src/core/Arena.ts"],"names":[],"mappings":"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"}
|