@series-inc/rundot-syncplay 5.23.0-beta.7

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 (264) hide show
  1. package/README.md +610 -0
  2. package/dist/animation.d.ts +119 -0
  3. package/dist/animation.js +488 -0
  4. package/dist/asset-cooking.d.ts +82 -0
  5. package/dist/asset-cooking.js +364 -0
  6. package/dist/authority-room.d.ts +217 -0
  7. package/dist/authority-room.js +701 -0
  8. package/dist/bot-documents.d.ts +115 -0
  9. package/dist/bot-documents.js +549 -0
  10. package/dist/bot-primitives.d.ts +152 -0
  11. package/dist/bot-primitives.js +472 -0
  12. package/dist/browser.d.ts +57 -0
  13. package/dist/browser.js +53 -0
  14. package/dist/canonical.d.ts +12 -0
  15. package/dist/canonical.js +288 -0
  16. package/dist/certification.d.ts +455 -0
  17. package/dist/certification.js +1855 -0
  18. package/dist/chaos-certification.d.ts +56 -0
  19. package/dist/chaos-certification.js +1397 -0
  20. package/dist/cjs/animation.js +495 -0
  21. package/dist/cjs/asset-cooking.js +372 -0
  22. package/dist/cjs/authority-room.js +704 -0
  23. package/dist/cjs/bot-documents.js +556 -0
  24. package/dist/cjs/bot-primitives.js +482 -0
  25. package/dist/cjs/browser.js +96 -0
  26. package/dist/cjs/canonical.js +299 -0
  27. package/dist/cjs/certification.js +1860 -0
  28. package/dist/cjs/chaos-certification.js +1402 -0
  29. package/dist/cjs/collections.js +36 -0
  30. package/dist/cjs/collision.js +96 -0
  31. package/dist/cjs/commands.js +987 -0
  32. package/dist/cjs/config-bundle.js +491 -0
  33. package/dist/cjs/creator.js +30 -0
  34. package/dist/cjs/deterministic-primitives.js +134 -0
  35. package/dist/cjs/deterministic-session.js +1410 -0
  36. package/dist/cjs/ecs-lite.js +155 -0
  37. package/dist/cjs/effects-adapter.js +97 -0
  38. package/dist/cjs/engine-complete.js +2297 -0
  39. package/dist/cjs/engine-core-proof.js +70 -0
  40. package/dist/cjs/engine-parity-matrix.js +874 -0
  41. package/dist/cjs/engine-parity.js +3183 -0
  42. package/dist/cjs/engine-runtime.js +3141 -0
  43. package/dist/cjs/errors.js +22 -0
  44. package/dist/cjs/events.js +247 -0
  45. package/dist/cjs/examples/counter-duel.js +157 -0
  46. package/dist/cjs/examples/fixed-arena.js +368 -0
  47. package/dist/cjs/index.js +229 -0
  48. package/dist/cjs/input-authority.js +150 -0
  49. package/dist/cjs/input-button.js +256 -0
  50. package/dist/cjs/input-timeline.js +418 -0
  51. package/dist/cjs/instant-replay.js +66 -0
  52. package/dist/cjs/lag-compensation3d.js +837 -0
  53. package/dist/cjs/local-authority-server-runner.js +1121 -0
  54. package/dist/cjs/local-authority-wire.js +1497 -0
  55. package/dist/cjs/local-authority.js +817 -0
  56. package/dist/cjs/local-ws-adapter.js +150 -0
  57. package/dist/cjs/match-log.js +61 -0
  58. package/dist/cjs/math.js +157 -0
  59. package/dist/cjs/mock-network-adapter.js +26 -0
  60. package/dist/cjs/movement.js +703 -0
  61. package/dist/cjs/movement3d.js +1246 -0
  62. package/dist/cjs/multiclient-authority.js +308 -0
  63. package/dist/cjs/multiplayer-service.js +434 -0
  64. package/dist/cjs/navigation.js +347 -0
  65. package/dist/cjs/navmesh.js +1437 -0
  66. package/dist/cjs/networked-client.js +691 -0
  67. package/dist/cjs/networked-input-authority.js +443 -0
  68. package/dist/cjs/node.js +47 -0
  69. package/dist/cjs/number-lab.js +78 -0
  70. package/dist/cjs/offline-session.js +370 -0
  71. package/dist/cjs/package.json +1 -0
  72. package/dist/cjs/pause-resume.js +43 -0
  73. package/dist/cjs/physics-cert.js +426 -0
  74. package/dist/cjs/physics2d.js +751 -0
  75. package/dist/cjs/physics3d.js +1996 -0
  76. package/dist/cjs/prediction-culling.js +203 -0
  77. package/dist/cjs/protocol-lab.js +1299 -0
  78. package/dist/cjs/r3f-render-adapter.js +209 -0
  79. package/dist/cjs/random.js +59 -0
  80. package/dist/cjs/replay-bundle.js +1598 -0
  81. package/dist/cjs/replay-inspect.js +236 -0
  82. package/dist/cjs/replay-scoring.js +68 -0
  83. package/dist/cjs/replay.js +356 -0
  84. package/dist/cjs/reports.js +181 -0
  85. package/dist/cjs/rollback-history.js +251 -0
  86. package/dist/cjs/runtime-guards.js +111 -0
  87. package/dist/cjs/sample-lab.js +145 -0
  88. package/dist/cjs/sample-scenes.js +752 -0
  89. package/dist/cjs/schema-artifacts.js +359 -0
  90. package/dist/cjs/schema-codegen.js +544 -0
  91. package/dist/cjs/schema-dsl-binary.js +655 -0
  92. package/dist/cjs/schema.js +319 -0
  93. package/dist/cjs/sdk-offline.js +99 -0
  94. package/dist/cjs/session-promotion.js +74 -0
  95. package/dist/cjs/session-snapshot.js +265 -0
  96. package/dist/cjs/session-wire.js +216 -0
  97. package/dist/cjs/signals.js +103 -0
  98. package/dist/cjs/sparse-set.js +169 -0
  99. package/dist/cjs/synctest.js +136 -0
  100. package/dist/cjs/system-lifecycle.js +286 -0
  101. package/dist/cjs/timers.js +61 -0
  102. package/dist/cjs/tools/static-checker.js +1468 -0
  103. package/dist/cjs/types.js +2 -0
  104. package/dist/cjs/venus-mp-budget.js +125 -0
  105. package/dist/cjs/ws-frame.js +255 -0
  106. package/dist/cli.d.ts +2 -0
  107. package/dist/cli.js +11379 -0
  108. package/dist/collections.d.ts +9 -0
  109. package/dist/collections.js +30 -0
  110. package/dist/collision.d.ts +25 -0
  111. package/dist/collision.js +85 -0
  112. package/dist/commands.d.ts +137 -0
  113. package/dist/commands.js +977 -0
  114. package/dist/config-bundle.d.ts +89 -0
  115. package/dist/config-bundle.js +483 -0
  116. package/dist/creator.d.ts +20 -0
  117. package/dist/creator.js +11 -0
  118. package/dist/deterministic-primitives.d.ts +59 -0
  119. package/dist/deterministic-primitives.js +126 -0
  120. package/dist/deterministic-session.d.ts +197 -0
  121. package/dist/deterministic-session.js +1401 -0
  122. package/dist/ecs-lite.d.ts +26 -0
  123. package/dist/ecs-lite.js +152 -0
  124. package/dist/effects-adapter.d.ts +21 -0
  125. package/dist/effects-adapter.js +94 -0
  126. package/dist/engine-complete.d.ts +45 -0
  127. package/dist/engine-complete.js +2292 -0
  128. package/dist/engine-core-proof.d.ts +26 -0
  129. package/dist/engine-core-proof.js +66 -0
  130. package/dist/engine-parity-matrix.d.ts +300 -0
  131. package/dist/engine-parity-matrix.js +869 -0
  132. package/dist/engine-parity.d.ts +74 -0
  133. package/dist/engine-parity.js +3178 -0
  134. package/dist/engine-runtime.d.ts +351 -0
  135. package/dist/engine-runtime.js +3123 -0
  136. package/dist/errors.d.ts +11 -0
  137. package/dist/errors.js +17 -0
  138. package/dist/events.d.ts +45 -0
  139. package/dist/events.js +242 -0
  140. package/dist/examples/counter-duel.d.ts +43 -0
  141. package/dist/examples/counter-duel.js +150 -0
  142. package/dist/examples/fixed-arena.d.ts +110 -0
  143. package/dist/examples/fixed-arena.js +356 -0
  144. package/dist/index.d.ts +101 -0
  145. package/dist/index.js +66 -0
  146. package/dist/input-authority.d.ts +82 -0
  147. package/dist/input-authority.js +147 -0
  148. package/dist/input-button.d.ts +36 -0
  149. package/dist/input-button.js +249 -0
  150. package/dist/input-timeline.d.ts +65 -0
  151. package/dist/input-timeline.js +412 -0
  152. package/dist/instant-replay.d.ts +59 -0
  153. package/dist/instant-replay.js +62 -0
  154. package/dist/lag-compensation3d.d.ts +146 -0
  155. package/dist/lag-compensation3d.js +827 -0
  156. package/dist/local-authority-server-runner.d.ts +79 -0
  157. package/dist/local-authority-server-runner.js +1118 -0
  158. package/dist/local-authority-wire.d.ts +142 -0
  159. package/dist/local-authority-wire.js +1477 -0
  160. package/dist/local-authority.d.ts +160 -0
  161. package/dist/local-authority.js +810 -0
  162. package/dist/local-ws-adapter.d.ts +14 -0
  163. package/dist/local-ws-adapter.js +146 -0
  164. package/dist/match-log.d.ts +74 -0
  165. package/dist/match-log.js +57 -0
  166. package/dist/math.d.ts +41 -0
  167. package/dist/math.js +154 -0
  168. package/dist/mock-network-adapter.d.ts +13 -0
  169. package/dist/mock-network-adapter.js +23 -0
  170. package/dist/movement.d.ts +155 -0
  171. package/dist/movement.js +694 -0
  172. package/dist/movement3d.d.ts +195 -0
  173. package/dist/movement3d.js +1238 -0
  174. package/dist/multiclient-authority.d.ts +30 -0
  175. package/dist/multiclient-authority.js +305 -0
  176. package/dist/multiplayer-service.d.ts +153 -0
  177. package/dist/multiplayer-service.js +429 -0
  178. package/dist/navigation.d.ts +91 -0
  179. package/dist/navigation.js +338 -0
  180. package/dist/navmesh.d.ts +236 -0
  181. package/dist/navmesh.js +1423 -0
  182. package/dist/networked-client.d.ts +124 -0
  183. package/dist/networked-client.js +688 -0
  184. package/dist/networked-input-authority.d.ts +51 -0
  185. package/dist/networked-input-authority.js +439 -0
  186. package/dist/node.d.ts +22 -0
  187. package/dist/node.js +19 -0
  188. package/dist/number-lab.d.ts +15 -0
  189. package/dist/number-lab.js +75 -0
  190. package/dist/offline-session.d.ts +2 -0
  191. package/dist/offline-session.js +367 -0
  192. package/dist/pause-resume.d.ts +48 -0
  193. package/dist/pause-resume.js +40 -0
  194. package/dist/physics-cert.d.ts +40 -0
  195. package/dist/physics-cert.js +389 -0
  196. package/dist/physics2d.d.ts +142 -0
  197. package/dist/physics2d.js +741 -0
  198. package/dist/physics3d.d.ts +285 -0
  199. package/dist/physics3d.js +1979 -0
  200. package/dist/prediction-culling.d.ts +34 -0
  201. package/dist/prediction-culling.js +200 -0
  202. package/dist/protocol-lab.d.ts +115 -0
  203. package/dist/protocol-lab.js +1295 -0
  204. package/dist/r3f-render-adapter.d.ts +61 -0
  205. package/dist/r3f-render-adapter.js +203 -0
  206. package/dist/random.d.ts +13 -0
  207. package/dist/random.js +55 -0
  208. package/dist/replay-bundle.d.ts +150 -0
  209. package/dist/replay-bundle.js +1586 -0
  210. package/dist/replay-inspect.d.ts +60 -0
  211. package/dist/replay-inspect.js +228 -0
  212. package/dist/replay-scoring.d.ts +28 -0
  213. package/dist/replay-scoring.js +65 -0
  214. package/dist/replay.d.ts +17 -0
  215. package/dist/replay.js +351 -0
  216. package/dist/reports.d.ts +176 -0
  217. package/dist/reports.js +174 -0
  218. package/dist/rollback-history.d.ts +46 -0
  219. package/dist/rollback-history.js +247 -0
  220. package/dist/runtime-guards.d.ts +1 -0
  221. package/dist/runtime-guards.js +108 -0
  222. package/dist/sample-lab.d.ts +32 -0
  223. package/dist/sample-lab.js +134 -0
  224. package/dist/sample-scenes.d.ts +86 -0
  225. package/dist/sample-scenes.js +745 -0
  226. package/dist/schema-artifacts.d.ts +49 -0
  227. package/dist/schema-artifacts.js +356 -0
  228. package/dist/schema-codegen.d.ts +21 -0
  229. package/dist/schema-codegen.js +538 -0
  230. package/dist/schema-dsl-binary.d.ts +75 -0
  231. package/dist/schema-dsl-binary.js +648 -0
  232. package/dist/schema.d.ts +87 -0
  233. package/dist/schema.js +315 -0
  234. package/dist/sdk-offline.d.ts +34 -0
  235. package/dist/sdk-offline.js +96 -0
  236. package/dist/session-promotion.d.ts +26 -0
  237. package/dist/session-promotion.js +70 -0
  238. package/dist/session-snapshot.d.ts +95 -0
  239. package/dist/session-snapshot.js +255 -0
  240. package/dist/session-wire.d.ts +89 -0
  241. package/dist/session-wire.js +211 -0
  242. package/dist/signals.d.ts +31 -0
  243. package/dist/signals.js +99 -0
  244. package/dist/sparse-set.d.ts +34 -0
  245. package/dist/sparse-set.js +165 -0
  246. package/dist/synctest.d.ts +31 -0
  247. package/dist/synctest.js +133 -0
  248. package/dist/system-lifecycle.d.ts +72 -0
  249. package/dist/system-lifecycle.js +281 -0
  250. package/dist/timers.d.ts +17 -0
  251. package/dist/timers.js +53 -0
  252. package/dist/tools/index.d.ts +4 -0
  253. package/dist/tools/index.js +4 -0
  254. package/dist/tools/static-checker.d.ts +31 -0
  255. package/dist/tools/static-checker.js +1462 -0
  256. package/dist/tools/vite-plugin.d.ts +69 -0
  257. package/dist/tools/vite-plugin.js +203 -0
  258. package/dist/types.d.ts +148 -0
  259. package/dist/types.js +1 -0
  260. package/dist/venus-mp-budget.d.ts +29 -0
  261. package/dist/venus-mp-budget.js +122 -0
  262. package/dist/ws-frame.d.ts +49 -0
  263. package/dist/ws-frame.js +247 -0
  264. package/package.json +169 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * M0 networked input-authority spike (§6). De-risks the committed Option 1 architecture:
3
+ * a game-agnostic input authority orders/confirms inputs from independent remote clients
4
+ * over a real socket, the server never runs the sim, and each client runs the deterministic
5
+ * sim with prediction/rollback. Under seeded packet loss and delay, both clients must end at
6
+ * identical confirmed checksums with self-verifying replays.
7
+ *
8
+ * Network conditions are modelled as logical per-tick delay/loss over the real WebSocket so
9
+ * the spike is reproducible in CI while still forcing the full predict/roll-back/resim loop.
10
+ */
11
+ export interface NetworkedInputAuthoritySpikeOptions {
12
+ readonly players: number;
13
+ readonly ticks: number;
14
+ readonly seed: number;
15
+ readonly hardToleranceTicks: number;
16
+ readonly maxPredictionTicks: number;
17
+ readonly lossPermille: number;
18
+ readonly minDelayTicks: number;
19
+ readonly maxDelayTicks: number;
20
+ readonly resendWindowTicks: number;
21
+ readonly redundancyWindowTicks: number;
22
+ }
23
+ export interface NetworkedInputAuthoritySpikeReport {
24
+ readonly players: number;
25
+ readonly ticks: number;
26
+ readonly seed: number;
27
+ readonly converged: boolean;
28
+ readonly confirmedThrough: number;
29
+ readonly confirmedStateChecksum: string;
30
+ readonly replayChecksum: string;
31
+ readonly clientChecksums: readonly string[];
32
+ readonly clientReplayChecksums: readonly string[];
33
+ readonly checksumsMatch: boolean;
34
+ readonly replaysMatch: boolean;
35
+ readonly replaysVerified: boolean;
36
+ /** Checksum from a clean sim fed only the authority's confirmed inputs — an independent oracle. */
37
+ readonly authorityOracleChecksum: string;
38
+ /** True when every client's confirmed state matches the independent oracle (catches symmetric bugs). */
39
+ readonly oracleMatch: boolean;
40
+ readonly totalRollbacks: number;
41
+ readonly maxRollbackDepth: number;
42
+ readonly meanRollbackDepth: number;
43
+ readonly maxConfirmationLagTicks: number;
44
+ readonly substitutedInputs: number;
45
+ readonly droppedInputMessages: number;
46
+ readonly droppedConfirmPackets: number;
47
+ readonly settleRounds: number;
48
+ readonly result: 'pass' | 'fail';
49
+ }
50
+ export declare const defaultNetworkedInputAuthoritySpikeOptions: NetworkedInputAuthoritySpikeOptions;
51
+ export declare function runNetworkedInputAuthoritySpike(options: NetworkedInputAuthoritySpikeOptions): Promise<NetworkedInputAuthoritySpikeReport>;
@@ -0,0 +1,439 @@
1
+ import { canonicalStringify, defaultChecksum } from './canonical.js';
2
+ import { invariant } from './errors.js';
3
+ import { createInputAuthority, } from './input-authority.js';
4
+ import { verifyReplay } from './replay.js';
5
+ import { decodeDeterministicSessionMessage, encodeDeterministicSessionMessage } from './session-wire.js';
6
+ import { canonicalizeLabInput, createLabSession, hydrateLabInput, labDefaultInput, labStep, } from './sample-lab.js';
7
+ import { connectWebSocketClient, startWebSocketServer, } from './ws-frame.js';
8
+ export const defaultNetworkedInputAuthoritySpikeOptions = {
9
+ players: 2,
10
+ ticks: 300,
11
+ seed: 71,
12
+ hardToleranceTicks: 6,
13
+ maxPredictionTicks: 8,
14
+ lossPermille: 80,
15
+ minDelayTicks: 1,
16
+ maxDelayTicks: 4,
17
+ resendWindowTicks: 6,
18
+ redundancyWindowTicks: 8,
19
+ };
20
+ const TAG_INPUT = 1;
21
+ const TAG_CONFIRM = 2;
22
+ // createLabSession's default tick rate; carried in session-start so a real client could pace itself.
23
+ const SESSION_TICK_RATE_HZ = 30;
24
+ export async function runNetworkedInputAuthoritySpike(options) {
25
+ invariant(options.players >= 2, 'spike requires at least two players', 'spike.players');
26
+ invariant(options.ticks >= 1, 'spike requires at least one tick', 'spike.ticks');
27
+ invariant(options.maxDelayTicks >= options.minDelayTicks && options.minDelayTicks >= 0, 'delay bounds must satisfy 0 <= min <= max', 'spike.delay-bounds');
28
+ const neutralInput = canonicalizeLabInput(labDefaultInput);
29
+ const authority = createInputAuthority({
30
+ playerCount: options.players,
31
+ hardToleranceTicks: options.hardToleranceTicks,
32
+ neutralInput,
33
+ maxFutureTicks: options.maxPredictionTicks + options.hardToleranceTicks + 4,
34
+ retainedConfirmedFrames: options.ticks + options.redundancyWindowTicks + 8,
35
+ });
36
+ const server = await startServer(authority, options);
37
+ try {
38
+ const clients = [];
39
+ for (let slot = 0; slot < options.players; slot += 1) {
40
+ const connection = await connectWebSocketClient(server.url);
41
+ clients.push(await createSpikeClient(connection, slot, options));
42
+ }
43
+ const settleCap = options.ticks + options.hardToleranceTicks + options.maxDelayTicks + 64;
44
+ let now = 0;
45
+ let settleRounds = 0;
46
+ for (; now < settleCap; now += 1) {
47
+ for (const client of clients) {
48
+ client.pump(now);
49
+ }
50
+ await flush();
51
+ server.pump(now);
52
+ await flush();
53
+ for (const client of clients) {
54
+ client.drain(now);
55
+ }
56
+ const done = clients.every((client) => client.appliedThrough >= options.ticks - 1 && client.predictedThrough >= options.ticks);
57
+ if (done) {
58
+ break;
59
+ }
60
+ if (now >= options.ticks) {
61
+ settleRounds += 1;
62
+ }
63
+ }
64
+ const confirmedTick = options.ticks - 1;
65
+ const converged = clients.every((client) => client.appliedThrough >= confirmedTick && client.predictedThrough >= options.ticks);
66
+ const clientChecksums = converged ? clients.map((client) => client.confirmedStateChecksum(options.ticks)) : [];
67
+ const replays = converged ? clients.map((client) => client.exportConfirmedReplay()) : [];
68
+ const clientReplayChecksums = replays.map((replay) => replayIdentity(replay));
69
+ const replaysVerified = converged && replays.every((replay) => verifyLabReplay(replay));
70
+ const checksumsMatch = converged && clientChecksums.every((checksum) => checksum === clientChecksums[0]);
71
+ const replaysMatch = converged && clientReplayChecksums.every((checksum) => checksum === clientReplayChecksums[0]);
72
+ // Independent oracle: replay ONLY the authority's confirmed inputs through a clean sim with no
73
+ // prediction/rollback. Comparing clients against it catches a symmetric rollback bug that would
74
+ // otherwise leave both clients identically wrong (both consume the same confirmed inputs).
75
+ const authorityOracleChecksum = converged ? computeAuthorityOracleChecksum(authority, options) : '';
76
+ const oracleMatch = converged && authorityOracleChecksum !== '' && clientChecksums.every((checksum) => checksum === authorityOracleChecksum);
77
+ const totalRollbacks = clients.reduce((sum, client) => sum + client.rollbacks, 0);
78
+ const maxRollbackDepth = clients.reduce((max, client) => Math.max(max, client.maxRollbackDepth), 0);
79
+ const rollbackDepthSum = clients.reduce((sum, client) => sum + client.rollbackDepthSum, 0);
80
+ const maxConfirmationLagTicks = clients.reduce((max, client) => Math.max(max, client.maxConfirmationLag), 0);
81
+ const pass = converged && checksumsMatch && replaysMatch && replaysVerified && oracleMatch;
82
+ return {
83
+ players: options.players,
84
+ ticks: options.ticks,
85
+ seed: options.seed,
86
+ converged,
87
+ confirmedThrough: authority.confirmedThrough,
88
+ confirmedStateChecksum: clientChecksums[0] ?? '',
89
+ replayChecksum: clientReplayChecksums[0] ?? '',
90
+ clientChecksums,
91
+ clientReplayChecksums,
92
+ checksumsMatch,
93
+ replaysMatch,
94
+ replaysVerified,
95
+ authorityOracleChecksum,
96
+ oracleMatch,
97
+ totalRollbacks,
98
+ maxRollbackDepth,
99
+ meanRollbackDepth: totalRollbacks === 0 ? 0 : Number((rollbackDepthSum / totalRollbacks).toFixed(3)),
100
+ maxConfirmationLagTicks,
101
+ substitutedInputs: authority.stats().substitutedInputs,
102
+ droppedInputMessages: server.droppedInputMessages(),
103
+ droppedConfirmPackets: clients.reduce((sum, client) => sum + client.droppedConfirmPackets, 0),
104
+ settleRounds,
105
+ result: pass ? 'pass' : 'fail',
106
+ };
107
+ }
108
+ finally {
109
+ await server.close();
110
+ }
111
+ }
112
+ async function startServer(authority, options) {
113
+ const connections = [];
114
+ const inbox = [];
115
+ let nextSlot = 0;
116
+ let droppedInputs = 0;
117
+ const handle = await startWebSocketServer({
118
+ path: '/authority',
119
+ onConnection(connection) {
120
+ const slot = nextSlot;
121
+ nextSlot += 1;
122
+ connections.push(connection);
123
+ connection.send(encodeDeterministicSessionMessage({
124
+ kind: 'session-start',
125
+ slot,
126
+ playerCount: options.players,
127
+ seed: options.seed,
128
+ tickRateHz: SESSION_TICK_RATE_HZ,
129
+ hardToleranceTicks: options.hardToleranceTicks,
130
+ role: 'player',
131
+ }));
132
+ connection.onMessage((text) => {
133
+ // Fail closed: malformed or unknown messages are dropped, never coerced.
134
+ const decoded = decodeDeterministicSessionMessage(text);
135
+ if (!decoded.ok || decoded.message.kind !== 'input') {
136
+ return;
137
+ }
138
+ const { slot: msgSlot, tick, input, attempt } = decoded.message;
139
+ // Server owns the slot mapping — reject input claiming a slot other than this connection's.
140
+ if (msgSlot !== slot) {
141
+ return;
142
+ }
143
+ const roll = networkRoll(options, TAG_INPUT, tick, slot, attempt);
144
+ if (roll.drop) {
145
+ droppedInputs += 1;
146
+ return;
147
+ }
148
+ inbox.push({ slot, tick, input, releaseAt: tick + roll.delay });
149
+ });
150
+ },
151
+ });
152
+ return {
153
+ url: handle.url,
154
+ pump(now) {
155
+ for (let index = inbox.length - 1; index >= 0; index -= 1) {
156
+ const entry = inbox[index];
157
+ if (entry.releaseAt <= now) {
158
+ authority.receiveInput(entry.slot, entry.tick, entry.input);
159
+ inbox.splice(index, 1);
160
+ }
161
+ }
162
+ authority.advance(now);
163
+ const from = Math.max(0, authority.confirmedThrough - options.redundancyWindowTicks + 1);
164
+ const frames = [];
165
+ for (let tick = from; tick <= authority.confirmedThrough; tick += 1) {
166
+ const frame = authority.confirmedFrame(tick);
167
+ if (frame !== undefined) {
168
+ frames.push(frame);
169
+ }
170
+ }
171
+ if (frames.length === 0) {
172
+ return;
173
+ }
174
+ const packet = encodeDeterministicSessionMessage({ kind: 'confirmed-input', authorityTick: now, frames });
175
+ for (const connection of connections) {
176
+ connection.send(packet);
177
+ }
178
+ },
179
+ droppedInputMessages() {
180
+ return droppedInputs;
181
+ },
182
+ async close() {
183
+ for (const connection of connections) {
184
+ connection.close();
185
+ }
186
+ await handle.close();
187
+ },
188
+ };
189
+ }
190
+ async function createSpikeClient(connection, slot, options) {
191
+ const session = createLabSession({ players: options.players, seed: options.seed, snapshotBufferSize: Math.max(256, options.ticks + 16) });
192
+ const timeline = [];
193
+ const ownInputs = new Map();
194
+ const confirmed = new Map();
195
+ const lastConfirmedBySlot = Array.from({ length: options.players }, () => canonicalizeLabInput(labDefaultInput));
196
+ const sendAttempts = new Map();
197
+ const inbox = [];
198
+ let predictedThrough = 0;
199
+ let appliedThrough = -1;
200
+ let rollbacks = 0;
201
+ let maxRollbackDepth = 0;
202
+ let rollbackDepthSum = 0;
203
+ let maxConfirmationLag = 0;
204
+ let droppedConfirmPackets = 0;
205
+ await new Promise((resolveWelcome) => {
206
+ connection.onMessage((text) => {
207
+ const decoded = decodeDeterministicSessionMessage(text);
208
+ if (!decoded.ok) {
209
+ return;
210
+ }
211
+ const message = decoded.message;
212
+ if (message.kind === 'session-start') {
213
+ resolveWelcome();
214
+ return;
215
+ }
216
+ if (message.kind === 'confirmed-input') {
217
+ const roll = networkRoll(options, TAG_CONFIRM, message.authorityTick, slot, 0);
218
+ if (roll.drop) {
219
+ droppedConfirmPackets += 1;
220
+ return;
221
+ }
222
+ inbox.push({ frames: message.frames, releaseAt: message.authorityTick + roll.delay, authorityNow: message.authorityTick });
223
+ }
224
+ });
225
+ });
226
+ function ensureOwnInput(tick) {
227
+ let own = ownInputs.get(tick);
228
+ if (own === undefined) {
229
+ own = ownInputForTick(options.seed, slot, tick);
230
+ ownInputs.set(tick, own);
231
+ }
232
+ return own;
233
+ }
234
+ function predictedInputForSlot(targetSlot, tick) {
235
+ // A confirmed tick is authoritative for every slot — including our own, whose real input
236
+ // the authority may have substituted (repeated) when it never arrived in time.
237
+ const conf = confirmed.get(tick);
238
+ if (conf !== undefined) {
239
+ return conf[targetSlot];
240
+ }
241
+ if (targetSlot === slot) {
242
+ return ensureOwnInput(tick);
243
+ }
244
+ return lastConfirmedBySlot[targetSlot];
245
+ }
246
+ function simulateTick(tick) {
247
+ const inputs = [];
248
+ for (let s = 0; s < options.players; s += 1) {
249
+ const input = predictedInputForSlot(s, tick);
250
+ inputs.push(input);
251
+ session.setInput(s, decodeLabInput(input));
252
+ }
253
+ timeline[tick] = inputs;
254
+ session.stepFrames(1);
255
+ }
256
+ function resimulateFrom(tick) {
257
+ // Frame/tick invariant: session snapshot at frame N is the state BEFORE tick N's inputs (after
258
+ // ticks 0..N-1). restoreToFrame(tick) leaves the session ready to re-apply tick `tick` onward.
259
+ session.restoreToFrame(tick);
260
+ for (let t = tick; t < predictedThrough; t += 1) {
261
+ const inputs = [];
262
+ for (let s = 0; s < options.players; s += 1) {
263
+ const input = predictedInputForSlot(s, t);
264
+ inputs.push(input);
265
+ session.setInput(s, decodeLabInput(input));
266
+ }
267
+ timeline[t] = inputs;
268
+ session.stepFrames(1);
269
+ }
270
+ }
271
+ function pump(now) {
272
+ while (predictedThrough < options.ticks && predictedThrough <= appliedThrough + options.maxPredictionTicks) {
273
+ const tick = predictedThrough;
274
+ const own = ensureOwnInput(tick);
275
+ simulateTick(tick);
276
+ predictedThrough = tick + 1;
277
+ sendInput(tick, own);
278
+ }
279
+ const resendFrom = Math.max(appliedThrough + 1, predictedThrough - options.resendWindowTicks);
280
+ for (let tick = resendFrom; tick < predictedThrough; tick += 1) {
281
+ const own = ownInputs.get(tick);
282
+ if (own !== undefined) {
283
+ sendInput(tick, own);
284
+ }
285
+ }
286
+ }
287
+ function sendInput(tick, input) {
288
+ const attempt = sendAttempts.get(tick) ?? 0;
289
+ sendAttempts.set(tick, attempt + 1);
290
+ connection.send(encodeDeterministicSessionMessage({ kind: 'input', slot, tick, input, attempt }));
291
+ }
292
+ function drain(now) {
293
+ for (let index = inbox.length - 1; index >= 0; index -= 1) {
294
+ const packet = inbox[index];
295
+ if (packet.releaseAt > now) {
296
+ continue;
297
+ }
298
+ inbox.splice(index, 1);
299
+ for (const frame of packet.frames) {
300
+ if (frame.tick <= appliedThrough) {
301
+ continue;
302
+ }
303
+ confirmed.set(frame.tick, frame.inputs);
304
+ }
305
+ maxConfirmationLag = Math.max(maxConfirmationLag, packet.authorityNow - appliedThrough);
306
+ }
307
+ applyConfirmed();
308
+ }
309
+ function applyConfirmed() {
310
+ while (confirmed.has(appliedThrough + 1)) {
311
+ const tick = appliedThrough + 1;
312
+ const confInputs = confirmed.get(tick);
313
+ const predInputs = timeline[tick];
314
+ if (predInputs === undefined || !equalInputs(confInputs, predInputs)) {
315
+ const depth = predictedThrough - tick;
316
+ if (depth > 0) {
317
+ rollbacks += 1;
318
+ rollbackDepthSum += depth;
319
+ maxRollbackDepth = Math.max(maxRollbackDepth, depth);
320
+ }
321
+ timeline[tick] = confInputs.slice();
322
+ if (depth > 0) {
323
+ resimulateFrom(tick);
324
+ }
325
+ else {
326
+ // Confirm leads prediction (a lagging client): the authority confirmed a tick we had not
327
+ // predicted yet. Extend the predicted head and step through it so the stepped session keeps
328
+ // pace with appliedThrough — mirrors the fix in createNetworkedSyncplayClient.
329
+ predictedThrough = tick + 1;
330
+ resimulateFrom(tick);
331
+ }
332
+ }
333
+ for (let s = 0; s < options.players; s += 1) {
334
+ lastConfirmedBySlot[s] = confInputs[s];
335
+ }
336
+ appliedThrough = tick;
337
+ }
338
+ }
339
+ return {
340
+ slot,
341
+ get predictedThrough() {
342
+ return predictedThrough;
343
+ },
344
+ get appliedThrough() {
345
+ return appliedThrough;
346
+ },
347
+ get rollbacks() {
348
+ return rollbacks;
349
+ },
350
+ get maxRollbackDepth() {
351
+ return maxRollbackDepth;
352
+ },
353
+ get rollbackDepthSum() {
354
+ return rollbackDepthSum;
355
+ },
356
+ get maxConfirmationLag() {
357
+ return maxConfirmationLag;
358
+ },
359
+ get droppedConfirmPackets() {
360
+ return droppedConfirmPackets;
361
+ },
362
+ pump,
363
+ drain,
364
+ confirmedStateChecksum(ticks) {
365
+ session.restoreToFrame(ticks);
366
+ return String(defaultChecksum(session.getState()));
367
+ },
368
+ exportConfirmedReplay() {
369
+ return session.exportReplay();
370
+ },
371
+ };
372
+ }
373
+ function computeAuthorityOracleChecksum(authority, options) {
374
+ const session = createLabSession({ players: options.players, seed: options.seed, snapshotBufferSize: options.ticks + 16 });
375
+ for (let tick = 0; tick < options.ticks; tick += 1) {
376
+ const frame = authority.confirmedFrame(tick);
377
+ if (frame === undefined) {
378
+ return '';
379
+ }
380
+ for (let s = 0; s < options.players; s += 1) {
381
+ session.setInput(s, hydrateLabInput(frame.inputs[s]));
382
+ }
383
+ session.stepFrames(1);
384
+ }
385
+ return String(defaultChecksum(session.getState()));
386
+ }
387
+ function ownInputForTick(seed, slot, tick) {
388
+ const h = mixHash([seed, slot, tick, 0x51ed]);
389
+ const dx = (h % 3) - 1;
390
+ const dy = (Math.floor(h / 3) % 3) - 1;
391
+ const action = (h & 1);
392
+ return canonicalizeLabInput({ dx, dy, action });
393
+ }
394
+ function decodeLabInput(input) {
395
+ return hydrateLabInput(input);
396
+ }
397
+ function equalInputs(left, right) {
398
+ return canonicalStringify(left) === canonicalStringify(right);
399
+ }
400
+ function replayIdentity(replay) {
401
+ return String(defaultChecksum(replay));
402
+ }
403
+ function verifyLabReplay(replay) {
404
+ const result = verifyReplay({
405
+ replay,
406
+ step: labStep,
407
+ defaultInput: labDefaultInput,
408
+ inputSchemaId: 'lab-input/v1',
409
+ stateSchemaId: 'lab-state/v1',
410
+ deterministicVersion: '0.0.0-source',
411
+ canonicalizeInput: canonicalizeLabInput,
412
+ hydrateInput: hydrateLabInput,
413
+ });
414
+ return result.mismatches === 0;
415
+ }
416
+ function networkRoll(options, tag, tick, slot, attempt) {
417
+ const h = mixHash([options.seed, tag, tick, slot, attempt]);
418
+ const drop = h % 1000 < options.lossPermille;
419
+ const span = options.maxDelayTicks - options.minDelayTicks + 1;
420
+ const delay = options.minDelayTicks + (mixHash([h, 0x9e3779b9]) % span);
421
+ return { drop, delay };
422
+ }
423
+ // FNV-1a variant with a per-step avalanche, over integer tuples. Used only for the seeded network
424
+ // model (loss/delay) — order-independent so results are reproducible regardless of arrival order.
425
+ function mixHash(values) {
426
+ let h = 0x811c9dc5;
427
+ for (const value of values) {
428
+ h ^= value | 0;
429
+ h = Math.imul(h, 0x01000193);
430
+ h ^= h >>> 15;
431
+ }
432
+ return h >>> 0;
433
+ }
434
+ // Yields to the event loop (not a timing delay — logical time is the discrete `now` counter) so
435
+ // real loopback WebSocket messages sent this round are delivered before the next round processes them.
436
+ async function flush() {
437
+ await new Promise((resolve) => setImmediate(resolve));
438
+ await new Promise((resolve) => setImmediate(resolve));
439
+ }
package/dist/node.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export * from './multiplayer-service.js';
2
+ export * from './input-authority.js';
3
+ export * from './local-authority-wire.js';
4
+ export * from './session-wire.js';
5
+ export * from './session-snapshot.js';
6
+ export * from './networked-client.js';
7
+ export { createOfflineSession } from './offline-session.js';
8
+ export * from './instant-replay.js';
9
+ export * from './pause-resume.js';
10
+ export * from './session-promotion.js';
11
+ export { verifyReplay } from './replay.js';
12
+ export * from './replay-scoring.js';
13
+ export * from './match-log.js';
14
+ export * from './authority-room.js';
15
+ export * from './ws-frame.js';
16
+ export { defaultNetworkedInputAuthoritySpikeOptions, runNetworkedInputAuthoritySpike, type NetworkedInputAuthoritySpikeOptions, type NetworkedInputAuthoritySpikeReport, } from './networked-input-authority.js';
17
+ export { runLocalWebSocketAdapterTest, startLocalWebSocketProtocolServer } from './local-ws-adapter.js';
18
+ export type { LocalWebSocketAdapterTestOptions, LocalWebSocketServer } from './local-ws-adapter.js';
19
+ export { explainChaosFixture, runChaosCertificationSuite } from './chaos-certification.js';
20
+ export type { ChaosCertificationOptions, ChaosCertificationReport, ChaosExpectedResult, ChaosFixtureCategory, ChaosFixtureExplanation, ChaosFixtureReport, } from './chaos-certification.js';
21
+ export { explainEngineParityFixture, runEngineParitySuite, stableEngineParityJson } from './engine-parity.js';
22
+ export type { EngineParityCategory, EngineParityCategoryReport, EngineParityFixtureExplanation, EngineParityMetric, EngineParityMetrics, EngineParityOptions, EngineParityProfile, EngineParityResult, EngineParitySuiteReport, EngineParityTier, } from './engine-parity.js';
package/dist/node.js ADDED
@@ -0,0 +1,19 @@
1
+ export * from './multiplayer-service.js';
2
+ export * from './input-authority.js';
3
+ export * from './local-authority-wire.js';
4
+ export * from './session-wire.js';
5
+ export * from './session-snapshot.js';
6
+ export * from './networked-client.js';
7
+ export { createOfflineSession } from './offline-session.js';
8
+ export * from './instant-replay.js';
9
+ export * from './pause-resume.js';
10
+ export * from './session-promotion.js';
11
+ export { verifyReplay } from './replay.js';
12
+ export * from './replay-scoring.js';
13
+ export * from './match-log.js';
14
+ export * from './authority-room.js';
15
+ export * from './ws-frame.js';
16
+ export { defaultNetworkedInputAuthoritySpikeOptions, runNetworkedInputAuthoritySpike, } from './networked-input-authority.js';
17
+ export { runLocalWebSocketAdapterTest, startLocalWebSocketProtocolServer } from './local-ws-adapter.js';
18
+ export { explainChaosFixture, runChaosCertificationSuite } from './chaos-certification.js';
19
+ export { explainEngineParityFixture, runEngineParitySuite, stableEngineParityJson } from './engine-parity.js';
@@ -0,0 +1,15 @@
1
+ export type NumberLabMode = 'integer' | 'fixed-q16' | 'quantized-float' | 'raw-float-basic' | 'raw-float-math';
2
+ export interface NumberLabOptions {
3
+ readonly mode: NumberLabMode;
4
+ readonly frames: number;
5
+ readonly seed: number;
6
+ }
7
+ export interface NumberLabResult {
8
+ readonly mode: NumberLabMode;
9
+ readonly frames: number;
10
+ readonly seed: number;
11
+ readonly finalValue: number;
12
+ readonly finalChecksum: string;
13
+ readonly result: 'pass';
14
+ }
15
+ export declare function runNumberLab(options: NumberLabOptions): NumberLabResult;
@@ -0,0 +1,75 @@
1
+ import { defaultChecksum } from './canonical.js';
2
+ import { DeterministicRandom } from './random.js';
3
+ const q16 = 65_536;
4
+ export function runNumberLab(options) {
5
+ validateOptions(options);
6
+ const random = new DeterministicRandom(options.seed);
7
+ let state = { value: 0, accumulator: 0, frame: 0 };
8
+ for (let frame = 0; frame < options.frames; frame += 1) {
9
+ state = stepNumberLab(options.mode, state, random, frame);
10
+ }
11
+ return {
12
+ mode: options.mode,
13
+ frames: options.frames,
14
+ seed: options.seed,
15
+ finalValue: state.value,
16
+ finalChecksum: defaultChecksum(state),
17
+ result: 'pass',
18
+ };
19
+ }
20
+ function stepNumberLab(mode, state, random, frame) {
21
+ const signed = random.nextInt(-1000, 1000);
22
+ if (mode === 'integer') {
23
+ return {
24
+ value: state.value + signed,
25
+ accumulator: state.accumulator + (signed % 17),
26
+ frame: frame + 1,
27
+ };
28
+ }
29
+ if (mode === 'fixed-q16') {
30
+ const delta = Math.trunc((signed * q16) / 1000);
31
+ return {
32
+ value: state.value + delta,
33
+ accumulator: state.accumulator + Math.trunc((delta * delta) / q16),
34
+ frame: frame + 1,
35
+ };
36
+ }
37
+ if (mode === 'quantized-float') {
38
+ const raw = state.value / 1000 + signed / 1000;
39
+ const quantized = Math.round(raw * 1000);
40
+ return {
41
+ value: quantized,
42
+ // rdm-ignore-next-line determinism/no-trig-math: number lab intentionally compares quantized float behavior outside gameplay runtime
43
+ accumulator: state.accumulator + Math.round(Math.sqrt(Math.abs(quantized)) * 1000),
44
+ frame: frame + 1,
45
+ };
46
+ }
47
+ if (mode === 'raw-float-basic') {
48
+ const value = state.value + signed / 1000 + 0.1;
49
+ return {
50
+ value,
51
+ accumulator: state.accumulator + value / 3,
52
+ frame: frame + 1,
53
+ };
54
+ }
55
+ const angle = (frame % 360) * (Math.PI / 180);
56
+ // rdm-ignore-next-line determinism/no-trig-math: raw-float-math mode intentionally demonstrates host math drift in diagnostics
57
+ const value = state.value + Math.sin(angle) * (signed / 1000) + Math.sqrt(Math.abs(signed));
58
+ return {
59
+ value,
60
+ // rdm-ignore-next-line determinism/no-trig-math: raw-float-math mode intentionally demonstrates host math drift in diagnostics
61
+ accumulator: state.accumulator + Math.cos(angle) + Math.atan2(signed, frame + 1),
62
+ frame: frame + 1,
63
+ };
64
+ }
65
+ function validateOptions(options) {
66
+ if (!['integer', 'fixed-q16', 'quantized-float', 'raw-float-basic', 'raw-float-math'].includes(options.mode)) {
67
+ throw new Error(`Unsupported number lab mode ${options.mode}`);
68
+ }
69
+ if (!Number.isInteger(options.frames) || options.frames < 1) {
70
+ throw new Error('number lab requires at least one frame');
71
+ }
72
+ if (!Number.isInteger(options.seed)) {
73
+ throw new Error('number lab seed must be an integer');
74
+ }
75
+ }
@@ -0,0 +1,2 @@
1
+ import type { DeterministicSessionConfig, OfflineSession } from './types.js';
2
+ export declare function createOfflineSession<State, Input>(config: DeterministicSessionConfig<State, Input>): OfflineSession<State, Input>;